// 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 306 2010-03-19 13:21:22Z panacoran $

using System;
using System.Collections;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
using Protra.Lib.Archiver;
using Protra.Lib.Config;
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
    {
        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;
            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 DownloadUtil downloadUtil;
        private DateTime date; // 処理中の日付
        private DateTime endDate; // 処理すべき最後の日付。
        
        private int numRecords; // 処理した一日分のレコード数
        private int totalRecords; // 処理すべき一日分のレコード数
        private int doneRecords; // 処理した総レコード数
        private int numDays; // 処理した日数
        private int totalDays; // 処理すべき日数
        private long startTime; // 処理の開始時刻
        private long timeToDownload; // ファイルのダウンロードにかかった時間
        private long downloadStart; // ダウンロードの開始時刻

        /// <summary>
        /// 更新に使用するDownloadUtilクラスのインスタンスを取得する。
        /// </summary>
        protected DownloadUtil DownloadUtil
        {
            get { return downloadUtil; }
        }

        /// <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 { return date; }
            set { date = value; }
        }
        
        /// <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>
        protected void StartDownload()
        {
            downloadStart = DateTime.Now.Ticks;
        }

        /// <summary>
        /// ファイルのダウンロードに要した時間を記録する。
        /// </summary>
        protected void EndDownload()
        {
            timeToDownload += DateTime.Now.Ticks - downloadStart;
        }

        /// <summary>
        /// 指定されたデータソースに対応する具象クラスのインスタンスを返す。
        /// </summary>
        /// <param name="option">オプションを指定する。</param>
        /// <returns></returns>
        static public PriceDataUpdator Create(UpdateConfig option)
        {
            PriceDataUpdator r;
            switch (option.PriceDataSource)
            {
                case PriceDataSource.KabukaJoho:
                    r = new KabukaJohoUpdator();
                    break;
                case PriceDataSource.Mujinzou:
                    r = new MujinzouUpdator(option.MujinzouUrl);
                    break;
                case PriceDataSource.KdbCom:
                    r = new KdbComUpdator();
                    break;
                case PriceDataSource.YahooFinance:
                    r = new YahooFinanceUpdator();
                    break;
                default:
                    return null;
            }
            r.downloadUtil = new DownloadUtil();
            if (option.UseProxy)
                r.downloadUtil.SetProxy(option.ProxyAddress, option.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のデータを取得できますが、" +
                        "マザーズの銘柄が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>
		/// 株価データを更新する。
		/// </summary>
		/// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        public abstract void Update(BackgroundWorker worker, DoWorkEventArgs e);

        /// <summary>
        /// 分割データを更新する。
        /// </summary>
        /// <param name="worker">BackgroundWroker</param>
        /// <param name="e">DoWrokイベントの引数</param>
        protected void UpdateSplitData(BackgroundWorker worker, DoWorkEventArgs e)
        {
            DateTime maxDate = SplitTable.MaxDate();
            if (maxDate > DateTime.MinValue &&
                    SplitTable.GetRecords(2814).Length == 0)
            {
                // データが間違えているので作り直し。
                SplitTable.Delete();
                maxDate = DateTime.MinValue;
            }
            ArrayList updated = ReadOmegaChartSplitData(worker, e, maxDate);
            if (updated == null)
                return;
            for (int i = 0; i < updated.Count; i++)
            {
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    return;
                }
                if (updated.Count > 100 && (i % 100 == 0 || i == updated.Count))
                    // 最初は時間がかかるので経過を表示する。
                    worker.ReportProgress(i * 100 / updated.Count, "株式分割データ");
                PriceData tmp = (PriceData)updated[i];
                SplitTable.Add(tmp.Code, tmp.Date, tmp.Split);
            }
        }

        /// <summary>
        /// OmegaChartの形式のindex.txtを読み込んで更新された分割データを返す。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        /// <param name="maxDate">既存のデータの最新の日付</param>
        private ArrayList ReadOmegaChartSplitData(BackgroundWorker worker, DoWorkEventArgs e, DateTime maxDate)
		{
			downloadUtil.IfModifiedSince = maxDate;
            downloadUtil.Url = "http://protra.sourceforge.jp/data/index.txt.lzh";
            Stream stream = downloadUtil.DownloadAndExtract();
            if (stream == null)
                return null;
            if (worker.CancellationPending)
            {
                e.Cancel = true;
                stream.Close();
                return null;
            }
            StreamReader reader = new StreamReader(stream, Encoding.GetEncoding("shift_jis"));
            ArrayList updated = new ArrayList();
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                int code = 0;
                foreach (string entry in line.Split(','))
                {
                    if (code == 0)
                    {
                        try
                        {
                            code = int.Parse(entry);
                        }
                        catch
                        {
                            break;
                        }
                        continue;
                    }
                    if (!entry.StartsWith("S:"))
                        continue;
                    int y = int.Parse(entry.Substring(2, 4));
                    int m = int.Parse(entry.Substring(6, 2));
                    int d = int.Parse(entry.Substring(8, 2));
                    DateTime date = new DateTime(y, m, d);
                    if (date <= maxDate)
                        continue;
                    PriceData r = new PriceData();
                    r.Code = code;
                    r.Date = date;
                    r.Split = double.Parse(entry.Substring(11));
                    updated.Add(r);
                }
            }
            reader.Close();
            return updated;
		}

        int lastProgress = -1; // 最初は日付の情報だけを表示する。

        /// <summary>
        /// 残りの所要時間と進捗を計算して表示する。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
		protected void UpdateProgress(BackgroundWorker worker, DoWorkEventArgs e)
		{
            int progress = Progress();
            if (lastProgress == progress)
                return;
            lastProgress = progress;
            string message = String.Format("{0:d} ({1}/{2}) ", date, numDays + 1, totalDays) + LeftTime();
            worker.ReportProgress(progress, message);
		}

        /// <summary>
        /// 進捗を計算して返す。
        /// </summary>
        /// <returns>%であらわした進捗</returns>
        public int Progress()
        {
            return (totalRecords == 0) ? 0 : numRecords * 100 / totalRecords;
        }

        /// <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 leftTime = 0;
            long timeToRecords = (DateTime.Now.Ticks - startTime) - timeToDownload;
            int downloaded = (numRecords > 0) ? numDays + 1 : numDays;
            if (downloaded > 0)
                leftTime = timeToDownload / downloaded * (totalDays - downloaded);
            if (doneRecords > 0)
            {
                int leftRecords = totalRecords - numRecords + (totalDays - numDays - 1) * totalRecords;
                leftTime += timeToRecords / doneRecords * leftRecords;
            }
            return (int)(leftTime / 10000000);
        }
	}
}
