// Copyright (C) 2005-2008, 2010, 2011 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 417 2011-03-24 03:05:55Z panacoran $

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

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

    /// <summary>
    /// 株価データをあらわすクラス。
    /// </summary>
    public class PriceData
    {
        private int code;
        private Db.MarketId marketId;
        private string name;
        private DateTime date;
        private int open;
        private int high;
        private int low;
        private int close;
        private double volume;
        private double split = 1;

        /// <summary>
        /// 銘柄コード
        /// </summary>
        public int Code
        {
            get { return code; }
            set { code = value; }
        }

        /// <summary>
        /// 市場ID
        /// </summary>
        public Db.MarketId MarketId
        {
            get { return marketId; }
            set { marketId = value; }
        }

        /// <summary>
        /// 銘柄名
        /// </summary>
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        /// <summary>
        /// 日付
        /// </summary>
        public DateTime Date
        {
            get { return date; }
            set { date = value; }
        }

        /// <summary>
        /// 始値
        /// </summary>
        public int Open
        {
            get { return open; }
            set { open = value; }
        }

        /// <summary>
        /// 高値
        /// </summary>
        public int High
        {
            get { return high; }
            set { high = value; }
        }

        /// <summary>
        /// 安値
        /// </summary>
        public int Low
        {
            get { return low; }
            set { low = value; }
        }

        /// <summary>
        /// 終値
        /// </summary>
        public int Close
        {
            get { return close; }
            set { close = value; }
        }

        /// <summary>
        /// 出来高
        /// </summary>
        public double Volume
        {
            get { return volume; }
            set { volume = value; }
        }

        /// <summary>
        /// 分割調整値
        /// </summary>
        public double Split
        {
            get { return split; }
            set { split = value; }
        }
    }

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

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        public DownloadUtil()
        {
            cookies = new CookieContainer();
            var u = GlobalEnv.UpdateConfig;
            if (u.UseProxy)
                proxy = new WebProxy(u.ProxyAddress, u.ProxyPort);
        }

        /// <summary>
        /// URLを設定する。
        /// </summary>
        public string Url { set; private get; }

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

        /// <summary>
        /// IfModifiedSinceを設定する。
        /// </summary>
        public DateTime IfModifiedSince { set; private get; }

        /// <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;
            request.UserAgent = "Protra Updator";
            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;
            }
            return response.GetResponseStream();
        }

        /// <summary>
        /// 設定に基づいてLHAで圧縮されたファイルをHTTPでダウンロードして展開する。
        /// </summary>
        /// <returns>展開したファイルを読むためのStream</returns>
        public Stream DownloadAndExtract()
        {
            var response = GetResponse();
            if (response == null)
                return null;
            if (!Url.EndsWith(".lzh", StringComparison.InvariantCultureIgnoreCase))
                return response;
            CleanUpTmpDir();
            string name = Url.Substring(Url.LastIndexOf('/') + 1);
            string archive = Path.Combine(Global.DirTmp, name);
            using (var fs = File.Create(archive))
            {
                int n;
                byte[] buf = new byte[4096];
                while ((n = response.Read(buf, 0, buf.Length)) > 0)
                    fs.Write(buf, 0, n);
            }
            response.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
	{
        /// <summary>
        /// 処理した一日分のレコード数を取得または設定する。
        /// </summary>
        protected int NumRecords { get; set; }

        /// <summary>
        /// 処理した日数を取得または設定する。
        /// </summary>
        protected int NumDays { get; set; }

        /// <summary>
        /// 処理したレコード数を取得または設定する。
        /// </summary>
        protected int DoneRecords { get; set; }

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

        /// <summary>
        /// 処理すべき日数を取得または設定する。
        /// </summary>
        protected int TotalDays { get; set; }

        /// <summary>
        /// 処理中の日付を取得または設定する。
        /// </summary>
        protected DateTime Date { get; set; }

        /// <summary>
        /// 処理すべき最後の日付を取得または設定する。
        /// </summary>
        protected DateTime EndDate { get; set; }

        private long startTicks; // 処理を開始したティック
        private long startDownload; // ダウンロードを開始したティック
        private long ticksToDownload = 0; // ダウンロードに要したティック

        /// <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;
            // 最も最近の市場が開いている日に設定する。
            for (EndDate = end; EndDate >= begin; EndDate = EndDate.AddDays(-1))
                if (Utils.IsMarketOpen(EndDate))
                    break;
            startTicks = DateTime.Now.Ticks;
        }

        /// <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;
            }
            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のデータを取得できますが、" +
                        "マザーズの銘柄が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;
            // 今日のデータがまだ置かれていない。
            if (!IsDataAvailable(today))
                today = today.AddDays(-1);
            var dl = new DownloadUtil();
            for (Start(begin, today); Date <= EndDate; NextDate())
            {
                NumRecords = 0; // 進捗の計算に使うのでここでリセットする。
                UpdateProgress(worker);
                ReadNameData();
                dl.Url = DownloadUrl();
                startDownload = DateTime.Now.Ticks;
                using (var stream = dl.DownloadAndExtract())
                {
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        return;
                    }
                    if (stream == null)
                    {
                        TotalDays--;
                        continue;
                    }
                    ticksToDownload += DateTime.Now.Ticks - startDownload;
                    var prices = new List<PriceData>();
                    ReadIndexData(prices);
                    using (var reader = new StreamReader(stream, Encoding.GetEncoding("shift_jis")))
                    {
                        string line;
                        while ((line = reader.ReadLine()) != null)
                        {
                            var tmp = ParseLine(line);
                            if (tmp != null)
                                prices.Add(tmp);
                        }
                    }
                    TotalRecords = prices.Count;
                    for (; NumRecords < TotalRecords; NumRecords++, DoneRecords++, UpdateProgress(worker))
                    {
                        if (worker.CancellationPending)
                        {
                            e.Cancel = true;
                            PriceTable.Delete(Date);
                            return;
                        }
                        PriceData d = prices[NumRecords];
                        Db.Brand b = BrandTable.GetRecordOrCreate(d.MarketId, d.Code, d.Name);
                        PriceTable.Add(b.Id, d.Date, d.Open, d.High, d.Low, d.Close, d.Volume);
                    }
                    NumDays++;
                }
            }
        }

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

        /// <summary>
        /// 銘柄名データを読む(KabukaJohoUpdator用)。
        /// </summary>
        protected virtual void ReadNameData()
        {}

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

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

        /// <summary>
        /// 指数のデータを読んで価格データを返す(KdbComUpdator用)。
        /// </summary>
        /// <param name="prices">価格データのリスト</param>
        protected virtual void ReadIndexData(List<PriceData> prices)
        {}

        /// <summary>
        /// index.txtを更新する。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        private void UpdateIndex(BackgroundWorker worker, DoWorkEventArgs e)
        {
            var dl = new DownloadUtil();
			dl.IfModifiedSince = GlobalEnv.BrandData.LastModified;
            dl.Url = "http://protra.sourceforge.jp/data/index.txt.lzh";
            Stream stream = dl.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();
		}

        int prevProgress = -1;
        int prevNumDyas = 0;
        long prevTicks = 0;

        /// <summary>
        /// 残りの所要時間と進捗を計算して表示する。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        protected void UpdateProgress(BackgroundWorker worker)
        {
            int progress = DoneRecords == 0 ? 0 : (int)((float)DoneRecords * 100 / (DoneRecords + TotalRecords - NumRecords + (TotalDays - NumDays - 1) * TotalRecords));
            if (progress > 100)
                progress = 100;
            if (progress == prevProgress && NumDays == prevNumDyas &&
                DateTime.Now.Ticks - prevTicks < (long)20 * 10000000) // 20秒以上経ったら残り時間を更新する。
                return;
            prevNumDyas = NumDays;
            prevProgress = progress;
            prevTicks = DateTime.Now.Ticks;
            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()
        {
            long leftTicks = 0;
            long ticksToRecords = (DateTime.Now.Ticks - startTicks) - ticksToDownload;
            leftTicks = ticksToDownload / (NumDays + 1) * (TotalDays - NumDays - 1);
            if (DoneRecords > 0)
            {
                int leftRecords = TotalRecords - NumRecords + (TotalDays - NumDays - 1) * TotalRecords;
                leftTicks += ticksToRecords / DoneRecords * leftRecords;
            }
            return (int)(leftTicks / 10000000);
        }
	}
}
