// Copyright (C) 2005-2008, 2010-2012 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 485 2013-07-18 01:57:32Z panacoran $

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
using SevenZip;
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
    {
        private readonly HttpWebRequest request;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public DownloadUtil(string url)
        {
            request = (HttpWebRequest) WebRequest.Create(url);
            var u = GlobalEnv.UpdateConfig;
            if (u.UseProxy)
                request.Proxy = new WebProxy(u.ProxyAddress, u.ProxyPort);
            request.UserAgent = "Protra Updator";
            SevenZipBase.SetLibraryPath(Path.Combine(Global.DirApp, IntPtr.Size == 4 ? "7z.dll" : "7z64.dll"));
        }

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

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

        /// <summary>
        /// HTTPリクエストの応答を返す。
        /// </summary>
        /// <returns>レスポンスを読むためのStream</returns>
        public Stream GetResponse()
        {
            try
            {
                var response = request.GetResponse().GetResponseStream();
                if (response != null) return response;
            }
            catch (WebException e)
            {
                if (e.Status == WebExceptionStatus.ProtocolError &&
                        ((HttpWebResponse)e.Response).StatusCode == HttpStatusCode.NotModified)
                    return null;
                throw;
            }
            return null;
        }

        /// <summary>
        /// LHAで圧縮されたファイルをダウンロードして展開して返す。
        /// </summary>
        /// <returns>展開したファイルを読むためのStream</returns>
        public Stream DownloadAndExtract()
        {
            var response = GetResponse();
            if (response == null) return null;
            var url = request.RequestUri.AbsolutePath;
            if (!url.EndsWith(".lzh", StringComparison.OrdinalIgnoreCase))
                return response;
// ReSharper disable AssignNullToNotNullAttribute
            var dst = Path.Combine(Global.DirTmp, Path.GetFileName(url));
// ReSharper restore AssignNullToNotNullAttribute
            var stream = new MemoryStream();
            using (response)
            using (var archive = File.Create(dst))
            {
                int n;
                var buf = new byte[4096];
                while ((n = response.Read(buf, 0, 4096)) > 0)
                    archive.Write(buf, 0, n);
                using (var extractor = new SevenZipExtractor(archive))
                {
                    if (extractor.ArchiveFileData.Count != 1)
                        throw new ApplicationException("データファイルの展開に失敗しました。");
                    extractor.ExtractFile(0, stream);
                }
            }
            File.Delete(dst);
            stream.Seek(0, SeekOrigin.Begin);
            return stream;
        }
    }

	/// <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; }

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

        private long startTicks; // 処理を開始したティック
        private long startDownload; // ダウンロードを開始したティック
        private long ticksToDownload; // ダウンロードに要したティック
        
        /// <summary>
        ///  処理の開始に必要な設定をする。
        /// <param name="begin">処理を開始する日付</param>
        /// <param name="end">処理を終了する日付</param>
        /// </summary>
        protected void Start(DateTime begin, DateTime end)
        {
            // 市場が開いている日数を数える。
            for (var d = begin; d <= end; d = d.AddDays(1))
                if (Calendar.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 (Calendar.IsMarketOpen(EndDate))
                    break;
            startTicks = DateTime.Now.Ticks;
        }

        /// <summary>
        /// 日付を次の市場が開いている日に進める。
        /// </summary>
        protected void NextDate()
        {
            do
            {
                Date = Date.AddDays(1);
            }
            while (!Calendar.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();
                    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[] {
                    "株価情報",
                    "無尽蔵",
                    "株価データダウンロードサイト",
                    "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のデータを取得できます。";
                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)
        {
            worker.ReportProgress(0, "銘柄データ");
            GlobalEnv.BrandData.Update(); // index.txtを更新する。
            if (worker.CancellationPending)
            {
                e.Cancel = true;
                return;
            }
            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)
        {
            var begin = (DateTime)e.Argument;
            if (begin < DataSince)
                begin = DataSince;
            var today = DateTime.Now;
            // 今日のデータがまだ置かれていない。
            if (!IsDataAvailable(today))
                today = today.AddDays(-1);
            for (Start(begin, today); Date <= EndDate; NextDate())
            {
                NumRecords = 0; // 進捗の計算に使うのでここでリセットする。
                UpdateProgress(worker);
                var dl = new DownloadUtil(DownloadUrl);
                startDownload = DateTime.Now.Ticks;
                var prices = new List<Price>();
                using (var stream = dl.DownloadAndExtract())
                {
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        PriceData.CloseAll();
                        return;
                    }
                    if (stream == null)
                    {
                        TotalDays--;
                        continue;
                    }
                    ticksToDownload += DateTime.Now.Ticks - startDownload;
                    using (var reader = new StreamReader(stream, Encoding.GetEncoding("shift_jis")))
                    {
                        string line;
                        var prev = new Price {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);
                    }
                }
                TotalRecords = prices.Count;
                for (; NumRecords < TotalRecords; NumRecords++, DoneRecords++, UpdateProgress(worker))
                {
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        PriceData.CloseAll();
                        return;
                    }
                    PriceData.Add(prices[NumRecords], Date == EndDate);
                }
                PriceData.MaxDate = Date;
                NumDays++;
            }
            PriceData.CloseAll();
        }

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


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

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

        /// <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 ticksToRecords = (DateTime.Now.Ticks - startTicks) - ticksToDownload;
            long 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);
        }
	}
}
