// Copyright (C) 2005-2008, 2010 panacorn <panacoran@users.sourceforge.jp>
// 
// This program is part of Protra.
//
// Protra is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <http://www.gnu.org/licenses/>.
// 
// $Id: PriceDataUpdator.cs 354 2010-04-14 09:32:30Z panacoran $

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using Protra.Lib.Archiver;
using Protra.Lib.Data;

namespace Protra.Lib.Update
{
	/// <summary>
	/// 株価データソースを示す列挙型です。
	/// </summary>
	public enum PriceDataSource
	{
		/// <summary>
		/// 株価情報
		/// </summary>
		KabukaJoho,
        /// <summary>
        /// 無尽蔵
        /// </summary>
        Mujinzou,
        /// <summary>
        /// 株価データダウンロードサイト
        /// </summary>
        KdbCom,
        /// <summary>
        /// Yahoo!ファイナンス
        /// </summary>
        YahooFinance
	}

    /// <summary>
    /// HTTPによるファイルのダウンロードと圧縮されたファイルの展開を行う。
    /// </summary>
    public class DownloadUtil
    {
        string url;
        string referer;
        WebProxy proxy;
        CookieContainer cookies;
        DateTime ifModifiedSince;
        Stream responseStream;

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        public DownloadUtil()
        {
            cookies = new CookieContainer();
        }

        /// <summary>
        /// URLを設定する。
        /// </summary>
        public string Url
        {
            set { url = value; }
        }

        /// <summary>
        /// refererを設定する。
        /// </summary>
        public string Referer
        {
            set { referer = value; }
        }

        /// <summary>
        /// プロキシを設定する。
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        public void SetProxy(string address, int port)
        {
            proxy = new WebProxy(address, port);
        }

        /// <summary>
        /// IfModifiedSinceを設定する。
        /// </summary>
        public DateTime IfModifiedSince
        {
            set { ifModifiedSince = value; }
        }

        /// <summary>
        /// 設定に基づいてHTTPリクエストを発行し、レスポンスを読むためのStreamを返す。
        /// </summary>
        /// <returns>レスポンスを読むためのStream</returns>
        public Stream GetResponse()
        {
            if (url == null)
                return null;
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            if (referer != null)
                request.Referer = referer;
            if (proxy != null)
                request.Proxy = proxy;
            request.CookieContainer = cookies;
            if (ifModifiedSince != DateTime.MinValue)
                request.IfModifiedSince = ifModifiedSince;
            HttpWebResponse response;
            try
            {
                response = (HttpWebResponse)request.GetResponse();
            }
            catch (WebException e)
            {
                if (e.Status == WebExceptionStatus.ProtocolError)
                    return null;
                throw;
            }
            if (response.StatusCode != HttpStatusCode.OK)
            {
                response.Close();
                return null;
            }
            responseStream = response.GetResponseStream();
            return responseStream;
        }

        /// <summary>
        /// 設定に基づいてLHAで圧縮されたファイルをHTTPでダウンロードして展開する。
        /// </summary>
        /// <returns>展開したファイルを読むためのStream</returns>
        public Stream DownloadAndExtract()
        {
            if (GetResponse() == null)
                return null;
            if (!url.EndsWith(".lzh", StringComparison.InvariantCultureIgnoreCase))
                return responseStream;
            CleanUpTmpDir();
            string name = url.Substring(url.LastIndexOf('/') + 1);
            string archive = Path.Combine(Global.DirTmp, name);
            using (FileStream fs = File.Create(archive))
            {
                int n;
                byte[] buf = new byte[4096];
                while ((n = responseStream.Read(buf, 0, buf.Length)) > 0)
                    fs.Write(buf, 0, n);
                fs.Close();
            }
            IExtracter e = new LhaExtracter();
            e.Extract(archive, Global.DirTmp);
            // 格納されているファイルの名前がおかしい場合があるので、
            // アーカイブファイルを消してからglob patternで釣り上げる。
            File.Delete(archive);
            string[] files = Directory.GetFiles(Global.DirTmp, "*");
            if (files == null || files.Length != 1)
                throw new ApplicationException("データファイルの展開に失敗しました。");
            return File.Open(files[0], FileMode.Open);
        }

        private void CleanUpTmpDir()
        {
            if (Directory.Exists(Global.DirTmp))
                Directory.Delete(Global.DirTmp, true);
            Directory.CreateDirectory(Global.DirTmp);
        }
    }

	/// <summary>
	/// 株価情報の更新を行う抽象クラスです。
	/// </summary>
	abstract public class PriceDataUpdator
	{
        private DateTime endDate; // 処理すべき最後の日付。
        
        private int numRecords; // 処理した一日分のレコード数
        private int totalRecords; // 処理すべき一日分のレコード数
        private int doneRecords; // 処理した総レコード数
        private int numDays; // 処理した日数
        private int totalDays; // 処理すべき日数
        private long startTime; // 処理の開始時刻

        /// <summary>
        /// 更新に使用するDownloadUtilオブジェクトを取得または設定する。
        /// </summary>
        protected DownloadUtil DownloadUtil { set; get; }     

        /// <summary>
        /// 処理した一日分のレコード数を取得する。
        /// </summary>
        protected int NumRecords
        {
            get { return numRecords; }
        }

        /// <summary>
        /// 処理すべき一日分のレコード数を取得または設定する。
        /// </summary>
        protected int TotalRecords
        {
            get { return totalRecords; }
            set { totalRecords = value; }
        }

        /// <summary>
        /// 処理したレコード数をインクリメントする。
        /// </summary>
        protected void IncrementRecords()
        {
            numRecords++;
            doneRecords++;
        }

        /// <summary>
        /// 処理した日数をインクリメントする。
        /// </summary>
        protected void IncrementDays()
        {
            numDays++;
            numRecords = 0;
        }

        /// <summary>
        /// 処理すべき日数をデクリメントする。
        /// </summary>
        protected void DecrementTotalDays()
        {
            totalDays--;
        }

        /// <summary>
        /// 処理中の日付を取得または設定する。
        /// </summary>
        protected DateTime Date { get; set; }
        
        /// <summary>
        ///  処理の開始に必要な設定をする。
        /// <param name="begin">処理を開始する日付</param>
        /// <param name="end">処理を終了する日付</param>
        /// </summary>
        protected void Start(DateTime begin, DateTime end)
        {
            for (DateTime d = begin; d <= end; d = d.AddDays(1))
                if (Utils.IsMarketOpen(d))
                {
                    if (Date == DateTime.MinValue)
                        Date = d; // 最初の市場が開いている日に設定する。
                    totalDays++;
                }
            if (Date == DateTime.MinValue) // begin > endの場合。
                Date = begin;
            endDate = end;
            startTime = DateTime.Now.Ticks;
        }

        /// <summary>
        /// 日付が終了日に達しているか？
        /// </summary>
        /// <returns></returns>
        protected bool ShouldContinue()
        {
            return Date <= endDate;
        }

        /// <summary>
        /// 日付を次の市場が開いている日に進める。
        /// </summary>
        protected void NextDate()
        {
            do
            {
                Date = Date.AddDays(1);
            }
            while (!Utils.IsMarketOpen(Date) && Date < endDate);
        }

        /// <summary>
        /// 指定されたデータソースに対応する具象クラスのインスタンスを返す。
        /// </summary>
        static public PriceDataUpdator Create()
        {
            var u = GlobalEnv.UpdateConfig;
            PriceDataUpdator r;
            switch (u.PriceDataSource)
            {
                case PriceDataSource.KabukaJoho:
                    r = new KabukaJohoUpdator();
                    break;
                case PriceDataSource.Mujinzou:
                    r = new MujinzouUpdator(u.MujinzouUrl);
                    break;
                case PriceDataSource.KdbCom:
                    r = new KdbComUpdator();
                    break;
                case PriceDataSource.YahooFinance:
                    r = new YahooFinanceUpdator();
                    break;
                default:
                    return null;
            }
            r.DownloadUtil = new DownloadUtil();
            if (u.UseProxy)
                r.DownloadUtil.SetProxy(u.ProxyAddress, u.ProxyPort);
            return r;
        }

        /// <summary>
        /// データソースの名前の一覧を取得する。
        /// </summary>
        static public string[] DataSourceNames
        {
            get
            {
                return new string[] {
                    "株価情報",
                    "無尽蔵",
                    "株価データダウンロードサイト",
                    "Yahoo!ファイナンス"
                };
            }
        }

        /// <summary>
        /// データソースの説明を取得する。
        /// </summary>
        /// <param name="dataSource">データソースを指定する</param>
        /// <returns>データソースの説明</returns>
        static public string GetDescription(PriceDataSource dataSource)
        {
            switch (dataSource)
            {
                case PriceDataSource.KabukaJoho:
                    return "2000年から2005年の東証とJasdaqのデータを取得できます。" +
                        "2006年からは東証、大証とJasdaqのデータを取得できます。";
                case PriceDataSource.Mujinzou:
                    return "1996年からの東証、大証とJasdaqのデータを取得できます。" +
                        "無尽蔵のメンバー専用です。メンバーの方はデータのURLを入力してください。";
                case PriceDataSource.KdbCom:
                    return "半年前からの東証、大証とJasdaqのデータを取得できます。";
                case PriceDataSource.YahooFinance:
                    return "1991年からの東証、大証とJasdaqのデータを取得できます。" +
                        "非常に時間がかかります。";
            }
            return "";
        }

        /// <summary>
        /// データが存在する最初の日付を取得する。
        /// </summary>
        public abstract DateTime DataSince
        {
            get;
        }

        /// <summary>
		/// 株価データとindex.txtを更新する。
		/// </summary>
		/// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        public void Update(BackgroundWorker worker, DoWorkEventArgs e)
        {
            UpdateIndex(worker, e);
            UpdatePrice(worker, e);
            worker.ReportProgress(100, "");
        }

        /// <summary>
        /// 株価データを更新する。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        protected virtual void UpdatePrice(BackgroundWorker worker, DoWorkEventArgs e)
        {
            DateTime begin = (DateTime)e.Argument;
            if (begin < DataSince)
                begin = DataSince;
            DateTime today = DateTime.Now;
            // 新しいデータが置かれるのは早くても午後4時以降
            if (!IsDataAvailable(today))
                today = today.AddDays(-1);
            DownloadUtil.IfModifiedSince = DateTime.MinValue; // 株価の更新では使わない。
            for (Start(begin, today); ShouldContinue(); NextDate())
			{
				UpdateProgress(worker);
                DownloadUtil.Url = DownloadUrl();
                Stream stream = DownloadUtil.DownloadAndExtract();
                if (stream == null)
                {
                    DecrementTotalDays();
                    continue;
                }
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    stream.Close();
                    return;
                }
                StreamReader reader = new StreamReader(stream, Encoding.GetEncoding("shift_jis"));
                string line;
                var prices = new List<Price>();
                var prev = new Price();
                prev.Code = "dummy";
                var notyet = false;
                string main_market = "dummy";
                while ((line = reader.ReadLine()) != null)
                {
                    Price tmp = ParseLine(line);
                    if (tmp == null)
                        continue;
                    if (prev.Code != tmp.Code)
                    {
                        main_market = GlobalEnv.BrandData[tmp.Code].Market.Substring(0, 1);
                        if (notyet)
                            prices.Add(prev);
                        notyet = true;
                        prev = tmp;
                    }
                    if (!notyet)
                        continue;
                    if (tmp.Market.Substring(0, 1) == main_market)
                    {
                        prices.Add(tmp);
                        notyet = false;
                        continue;
                    }
                    // 優先市場がわからない場合には出来高の大きなほうを優先する。
                    if (tmp.Volume > prev.Volume)
                        prev = tmp;
                }
                if (notyet)
                    prices.Add(prev);
                reader.Close();
                TotalRecords = prices.Count;
                for (; NumRecords < TotalRecords; IncrementRecords(), UpdateProgress(worker))
                {
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        return;
                    }
                    PriceData.Add(prices[NumRecords]);
                }
                PriceData.MaxDate = Date;
                IncrementDays();
            }
        }

        /// <summary>
        /// 新しいデータが置かれる時刻に達しているか。
        /// </summary>
        /// <param name="time">時刻</param>
        /// <returns></returns>
        protected abstract bool IsDataAvailable(DateTime time);

        /// <summary>
        /// データのURLを取得する。
        /// </summary>
        /// <returns>URL</returns>
        protected abstract string DownloadUrl();

        /// <summary>
        /// 文字列を解析して価格データを返す。
        /// </summary>
        /// <param name="line">文字列</param>
        /// <returns>価格データ</returns>
        protected abstract Price ParseLine(string line);

        /// <summary>
        /// index.txtを更新する。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        private void UpdateIndex(BackgroundWorker worker, DoWorkEventArgs e)
        {
            DownloadUtil.IfModifiedSince = GlobalEnv.BrandData.LastModified.AddHours(1);
            DownloadUtil.Url = "http://protra.sourceforge.jp/data/index.txt.lzh";
            Stream stream = DownloadUtil.DownloadAndExtract();
            if (stream == null)
                return;
            if (worker.CancellationPending)
            {
                e.Cancel = true;
                stream.Close();
                return;
            }
            stream.Close();
            File.Delete(Path.Combine(Global.DirData, "index.txt"));
            GlobalEnv.BrandData.Load();
            GlobalEnv.BrandListConfig.SetDefaultBrandList(); // A500とN225の変更を反映させる。
		}

        static int prev_progress = -1;

        /// <summary>
        /// 残りの所要時間と進捗を計算して表示する。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
		protected void UpdateProgress(BackgroundWorker worker)
		{
            int progress = doneRecords == 0 ? 0 : (int)((float)doneRecords / totalDays / totalRecords * 100);
            if (progress > 100)
                progress = 100;
            if (progress == prev_progress)
                return;
            prev_progress = progress;
            string message = String.Format("{0:d} ({1}/{2}) ", Date, numDays + 1, totalDays) + LeftTime();
            worker.ReportProgress(progress, message);
		}

        /// <summary>
        /// 残り時間を文字列表現で返す。
        /// </summary>
        /// <returns>文字列で表した残り時間</returns>
        private string LeftTime()
        {
            int leftTime = CalcLeftTime();
            if (leftTime == 0) // 処理が始まる前も0になる。
                return "";
            string s = "残り";
            if (leftTime <= 50)
                s += " " + leftTime + " 秒";
            else
            {
                if (leftTime >= 3600)
                {
                    s += " " + (leftTime / 3600) + " 時間";
                    leftTime %= 3600;
                }
                if (leftTime > 0)
                    s += " " + ((leftTime + 59) / 60) + " 分"; // 切り上げ
            }
            return s;
        }

        /// <summary>
        /// 残り時間を計算する。
        /// </summary>
        /// <returns>秒数であらわした残り時間</returns>
        private int CalcLeftTime()
        {
            if (doneRecords == 0)
                return 0;
            return (int)((DateTime.Now.Ticks - startTime) / doneRecords * (totalRecords - numRecords + (totalDays - numDays - 1) * totalRecords) / 10000000);
        }
	}
}
