// Copyright (C) 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: YahooFinanceUpdator.cs 285 2010-03-06 04:33:43Z panacoran $

using System;
using System.Collections;
using System.ComponentModel;
using System.Text;
using System.Text.RegularExpressions;
using System.Globalization;
using System.IO;
using System.Net;
using Protra.Lib.Db;

namespace Protra.Lib.Update
{
	/// <summary>
	/// Yahoo!ファイナンスを利用して株価データを更新するクラス。
	/// </summary>
    public class YahooFinanceUpdator: PriceDataUpdator
    {
        Regex regex;

        /// <summary>
        /// データが存在する最初の日付を取得する。
        /// </summary>
        public override DateTime DataSince
        {
            get { return new DateTime(1991, 1, 4); }
        }

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        public YahooFinanceUpdator()
        {
            regex = new Regex("<b class=\"yjXL\">(?<name>[^<]+)</b><span class=\"yjM\">" +
                "【(?<market>.*): (?<code>[0-9]+)(?:\\.[TOQNJ])?】.*" +
                "<td><small>(?<year>\\d{4})年(?<month>1?\\d)月(?<day>\\d?\\d)日</small></td>\\n" +
                "<td><small>(?<open>[0-9,.]+)</small></td>\\n" +
                "<td><small>(?<high>[0-9,.]+)</small></td>\\n" +
                "<td><small>(?<low>[0-9,.]+)</small></td>\\n" +
                "<td><small><b>(?<close>[0-9,.]+)</b></small></td>\\n" +
                "(?:<td><small>(?<volume>[0-9,]+)</small></td>)?",
                RegexOptions.Compiled | RegexOptions.Singleline);
        }

        /// <summary>
        /// 株価データを更新します。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        public override void Update(BackgroundWorker worker, DoWorkEventArgs e)
        {
            DateTime begin = (DateTime)e.Argument;
            if (begin < DataSince)
                begin = DataSince;
            DateTime today = DateTime.Now;
            // 新しいデータが置かれるのは早くても午後7時以降
            if (today.Hour < 19)
                today = today.AddDays(-1);

            for (Start(begin, today); ShouldContinue(); NextDate())
            {
                UpdateProgress(worker, e);

                TotalRecords = 9999 - 1300 + 2;
                for (; NumRecords < TotalRecords; IncrementRecords(), UpdateProgress(worker, e))
                {
                    if (worker.CancellationPending)
                    {
                        e.Cancel = true;
                        PriceTable.Delete(Date);
                        return;
                    }
                    int code;
                    if (NumRecords == 0)
                        code = 998407; // 日経225
                    else if (NumRecords == 1)
                        code = 998405; // TOPIX
                    else
                        code = NumRecords - 2 + 1301;
                    PriceData data = ParsePage(GetPage(code));
                    if (data == null)
                    {
                        if (NumRecords == 0) // 日経平均がない。
                            break;
                        else
                            continue;
                    }
                    Brand brand = BrandTable.GetRecordOrCreate(data.MarketId, data.Code, data.Name);
                    DateTime latest = PriceTable.MaxDateById(brand.Id);
                    if (latest != DateTime.MinValue)
                    {
                        for (DateTime d = latest.AddDays(1); d < data.Date; d = d.AddDays(1))
                        {
                            if (!Utils.IsMarketOpen(d))
                                continue;
                            PriceTable.Add(brand.Id, d, 0, 0, 0, 0, 0);
                        }
                    }
                    PriceTable.Add(brand.Id, data.Date, data.Open, data.High, data.Low, data.Close, data.Volume);
                }
                if (NumRecords == TotalRecords)
                    IncrementDays();
                else
                    DecrementTotalDays();
            }

            UpdateSplitData(worker, e);
            worker.ReportProgress(100, "");
        }

        private string GetPage(int code)
        {
            DownloadUtil.Url =
                "http://table.yahoo.co.jp/t?c=" + Date.Year + "&a=" + Date.Month + "&b=" + Date.Day +
                "&f=" + Date.Year + "&d=" + Date.Month + "&e=" + Date.Day + "&g=d" + "&s=" + code +
                "&y=0" + "&z=" + code;
            for (int i = 0; i < 10; i++)
            {
                try
                {
                    Stream stream = DownloadUtil.GetResponse();
                    if (stream == null)
                        return null;
                    StreamReader reader = new StreamReader(stream, Encoding.GetEncoding("euc-jp"));
                    string buf = reader.ReadToEnd();
                    reader.Close();
                    return buf;
                }
                catch (WebException e)
                {
                    switch (e.Status)
                    {
                        case WebExceptionStatus.Timeout:
                        case WebExceptionStatus.ConnectionClosed:
                        case WebExceptionStatus.ReceiveFailure:
                        case WebExceptionStatus.ConnectFailure:
                            break;
                        default:
                            throw;
                    }
                }
            }
            return null;
        }

        private PriceData ParsePage(string buf)
        {
            if (buf == null)
                return null;
            Match m = regex.Match(buf);
            if (!m.Success)
                return null;
            PriceData r = new PriceData();
            try
            {
                r.Date = new DateTime(int.Parse(m.Groups["year"].Value),
                    int.Parse(m.Groups["month"].Value),
                    int.Parse(m.Groups["day"].Value));
                r.Code = int.Parse(m.Groups["code"].Value);
                r.Name = m.Groups["name"].Value;
                r.Name = r.Name.Replace("(株)", null);
                NumberStyles s = NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands;
                r.Open = (int)double.Parse(m.Groups["open"].Value, s);
                r.High = (int)double.Parse(m.Groups["high"].Value, s);
                r.Low = (int)double.Parse(m.Groups["low"].Value, s);
                r.Close = (int)double.Parse(m.Groups["close"].Value, s);
                if (m.Groups["volume"].Value != "")
                    r.Volume = double.Parse(m.Groups["volume"].Value, s) / 1000;
                switch (m.Groups["market"].Value)
                {
                    case "東証1部":
                    case "東証":
                    case "---":
                        r.MarketId = MarketId.Tokyo1;
                        break;
                    case "東証2部":
                        r.MarketId = MarketId.Tokyo2;
                        break;
                    case "マザーズ":
                        r.MarketId = MarketId.Mothers;
                        break;
                    case "東証外国":
                        r.MarketId = MarketId.TokyoForeign;
                        break;
                    case "大証":
                    case "大証1部":
                        r.MarketId = MarketId.Osaka1;
                        break;
                    case "大証2部":
                        r.MarketId = MarketId.Osaka2;
                        break;
                    case "ヘラクレス":
                        r.MarketId = MarketId.Hercules;
                        break;
                    case "名証":
                    case "名証1部":
                        r.MarketId = MarketId.Nagoya1;
                        break;
                    case "名証2部":
                        r.MarketId = MarketId.Nagoya2;
                        break;
                    case "JASDAQ":
                        r.MarketId = MarketId.Jasdaq;
                        break;
                    default:
                        return null;
                }
            }
            catch (FormatException)
            {
                return null;
            }
            // 無尽蔵に合わせる。
            if (r.Code == 998407)
            {
                r.Code = 1001;
                r.MarketId = MarketId.Tokyo1;
            }
            else if (r.Code == 998405)
                r.Code = 1002;
            return r;
        }
    }
}
