// Copyright (C) 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: YahooFinanceUpdator.cs 479 2013-07-05 06:08:50Z panacoran $

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

namespace Protra.Lib.Update
{
    /// <summary>
    /// Yahoo!ファイナンスを利用して株価データを更新するクラス。
    /// </summary>
    public class YahooFinanceUpdator : PriceDataUpdator
    {
        private readonly Object _syncObject = new object();
        private Queue<Brand> _brandQueue;
        private readonly Queue<Price> _priceQueue = new Queue<Price>();
        private bool _terminate;
        private Exception _exception;

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

        /// <summary>
        /// 株価データを更新します。
        /// </summary>
        /// <param name="worker">BackgroundWorker</param>
        /// <param name="e">DoWorkイベントの引数</param>
        protected override void UpdatePrice(BackgroundWorker worker, DoWorkEventArgs e)
        {
            var begin = (DateTime)e.Argument;
            if (begin < DataSince)
                begin = DataSince;
            var today = DateTime.Now;
            // 新しいデータが置かれるのは早くても午後7時以降
            if (today.Hour < 19)
                today = today.AddDays(-1);
            var threads = new Thread[1];
            for (var i = 0; i < threads.Length; i++)
                (threads[i] = new Thread(DoFetchPrice) {Name = "Fetch Thread " + i}).Start();
            try
            {
                for (Start(begin, today); Date <= EndDate; NextDate())
                {
                    TotalRecords = GlobalEnv.BrandData.Count;
                    UpdateProgress(worker);
                    lock (_syncObject)
                    {
                        _brandQueue = new Queue<Brand>(GlobalEnv.BrandData);
                        Monitor.PulseAll(_syncObject);
                    }
                    for (NumRecords = 0; NumRecords < TotalRecords; NumRecords++, DoneRecords++, UpdateProgress(worker))
                    {
                        if (worker.CancellationPending)
                        {
                            e.Cancel = true;
                            return;
                        }
                        Price price;
                        lock (_priceQueue)
                        {
                            while (_priceQueue.Count == 0 && _exception == null)
                                Monitor.Wait(_priceQueue);
                            if (_exception != null)
                                throw _exception;
                            price = _priceQueue.Dequeue();
                        }
                        if (price == null) // 上場廃止
                            continue;
                        if (price.Code == "1001" && price.Open == 0) // まだ株価が用意されていない。
                            return;
                        PriceData.Add(price, Date == EndDate);
                    }
                    if (NumRecords == TotalRecords)
                    {
                        PriceData.MaxDate = Date;
                        NumDays++;
                    }
                    else
                        TotalDays--;
                }
            }
            finally
            {
                lock (_syncObject)
                {
                    _terminate = true;
                    Monitor.PulseAll(_syncObject);
                }
                foreach (var thread in threads)
                    thread.Join();
                PriceData.CloseAll();
            }
        }

        private void DoFetchPrice()
        {
            var code = "";
            try
            {
                while (true)
                {
                    lock (_syncObject)
                    {
                        while ((_brandQueue == null || _brandQueue.Count == 0) && !_terminate)
                            Monitor.Wait(_syncObject);
                        if (_terminate || _brandQueue == null)
                            return;
                        code = _brandQueue.Dequeue().Code;
                    }
                    var price = ParsePage(GetPage(code));
                    if (price != null)
                        price.Code = code;
                    lock (_priceQueue)
                    {
                        _priceQueue.Enqueue(price);
                        Monitor.Pulse(_priceQueue);
                    }
                }
            }
            catch (Exception e)
            {
                lock (_priceQueue)
                {
                    _exception = new Exception(string.Format("{0}: {1} {2:d}", e.Message, code, Date), e);
                    Monitor.Pulse(_priceQueue);
                }
            }
        }

        private string GetPage(string code)
        {
            if (code == "1001")
                code = "998407";
            else if (code == "1002")
                code = "998405";
            var dl = new DownloadUtil(string.Format(
                "http://info.finance.yahoo.co.jp/history/?code={0}&sy={1}&sm={2}&sd={3}&ey={4}&em={5}&ed={6}&tm=d",
                code, Date.Year, Date.Month, Date.Day, Date.Year, Date.Month, Date.Day));
            for (var i = 0; i < 10; i++)
            {
                try
                {
                    var stream = dl.GetResponse();
                    if (stream == null)
                        return null;
                    using (var reader = new StreamReader(stream))
                        return reader.ReadToEnd();
                }
                catch (WebException e)
                {
                    switch (e.Status)
                    {
                        case WebExceptionStatus.Timeout:
                        case WebExceptionStatus.ConnectionClosed:
                        case WebExceptionStatus.ReceiveFailure:
                        case WebExceptionStatus.ConnectFailure:
                            break;
                        default:
                            throw;
                    }
                }
            }
            throw new Exception(string.Format("ページの取得に失敗しました。"));
        }

        /// <summary>
        /// Webページをパースして株価データを取り出す。
        /// </summary>
        /// <param name="buf">Webページの文字列</param>
        /// <returns>上場廃止ならnullを返す。出来高がないか当日の株価がまだ用意されていないなら数字が0のデータを返す。それ以外は取り出した株価データを返す。</returns>
        /// <exception cref="Exception">Webページのフォーマットが想定と違う場合にスローされる。</exception>
        private Price ParsePage(string buf)
        {
            var valid = new Regex(
                @"<td>(?<year>\d{4})年(?<month>1?\d)月(?<day>\d?\d)日</td>" +
                "<td>(?<open>[0-9,.]+)</td><td>(?<high>[0-9,.]+)</td><td>(?<low>[0-9,.]+)</td>" +
                "<td>(?<close>[0-9,.]+)</td>(?:<td>(?<volume>[0-9,]+)</td>)?");
            var invalid = new Regex("該当する期間のデータはありません。<br>期間をご確認ください。");
            var obs = new Regex("該当する銘柄はありません。<br>再度銘柄（コード）を入力し、「表示」ボタンを押してください。");
            var empty = new Regex("<dl class=\"stocksInfo\">\n<dt></dt><dd class=\"category yjSb\"></dd>");

            if (buf == null)
                return null;
            var m = valid.Match(buf);
            if (!m.Success)
            {
                if (obs.Match(buf).Success || empty.Match(buf).Success) // 上場廃止(銘柄データが空のこともある)
                    return null;
                if (invalid.Match(buf).Success) // 出来高がないか株価が用意されていない。
                    return new Price {Date = Date, Open = 0, High = 0, Low = 0, Close = 0, Volume = 0.0};
                throw new Exception("ページから株価を取得できません。");
            }
            try
            {
                const NumberStyles s = NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands;
                return new Price
                    {
                        Date = new DateTime(int.Parse(m.Groups["year"].Value),
                                            int.Parse(m.Groups["month"].Value),
                                            int.Parse(m.Groups["day"].Value)),
                        Open = (int)double.Parse(m.Groups["open"].Value, s),
                        High = (int)double.Parse(m.Groups["high"].Value, s),
                        Low = (int)double.Parse(m.Groups["low"].Value, s),
                        Close = (int)double.Parse(m.Groups["close"].Value, s),
                        Volume = m.Groups["volume"].Value == "" ? 0.0 : double.Parse(m.Groups["volume"].Value, s) / 1000
                    };
            }
            catch (FormatException e)
            {
                throw new Exception("ページから株価を取得できません。", e);
            }
        }

        /// <summary>
        /// 使わない
        /// </summary>
        /// <param name="time">使わない</param>
        /// <returns>使わない</returns>
        protected override bool IsDataAvailable(DateTime time)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 使わない
        /// </summary>
        /// <returns>使わない</returns>
        protected override string DownloadUrl
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// 使わない
        /// </summary>
        /// <param name="line">使わない</param>
        /// <returns>使わない</returns>
        protected override Price ParseLine(string line)
        {
            throw new NotImplementedException();
        }
    }
}