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

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows.Forms;
using Protra.Lib.Dialogs;

namespace Protra.Lib.Data
{
    /// <summary>
    /// 株価データをあらわすクラス。
    /// </summary>
    public class Price
    {
        /// <summary>
        /// レコードサイズ
        /// </summary>
        public static readonly int RecordSize = 4 /* date */+ 4 * 4 /* prices */+ 8 /* volume */;

        /// <summary>
        /// 証券コード
        /// </summary>
        public string Code { get; set; }

        /// <summary>
        /// 市場
        /// </summary>
        public string Market { get; set; }

        /// <summary>
        /// 日付
        /// </summary>
        public DateTime Date { get; set; }

        /// <summary>
        /// 始値
        /// </summary>
        public int Open { get; set; }

        /// <summary>
        /// 高値
        /// </summary>
        public int High { get; set; }

        /// <summary>
        /// 安値
        /// </summary>
        public int Low { get; set; }

        /// <summary>
        /// 終値
        /// </summary>
        public int Close { get; set; }

        /// <summary>
        /// 出来高
        /// </summary>
        public double Volume { get; set; }

        /// <summary>
        /// 分割比率を適用する。
        /// </summary>
        public void Split(double ratio)
        {
            Open = (int)(Open / ratio);
            High = (int)(High / ratio);
            Low = (int)(Low / ratio);
            Close = (int)(Close / ratio);
            Volume *= ratio;
        }

        /// <summary>
        /// 価格データを読み込む。
        /// </summary>
        /// <param name="b">BinaryStream</param>
        public void Read(BinaryReader b)
        {
            Date = new DateTime((long)b.ReadInt32() * 86400 * 10000000);
            Open = b.ReadInt32();
            High = b.ReadInt32();
            Low = b.ReadInt32();
            Close = b.ReadInt32();
            Volume = b.ReadDouble();
        }

        /// <summary>
        /// 価格データを書き込む。
        /// </summary>
        /// <param name="b">BinaryStream</param>
        public void Write(BinaryWriter b)
        {
            b.Write((int)(Date.Ticks / 86400 / 10000000));
            b.Write(Open);
            b.Write(High);
            b.Write(Low);
            b.Write(Close);
            b.Write(Volume);
        }
    }

    /// <summary>
    /// 株価データのタイムフレームを表す列挙体
    /// </summary>
    public enum TimeFrame
    {
        /// <summary>
        /// 日足
        /// </summary>
        Daily,

        /// <summary>
        /// 週足
        /// </summary>
        Weekly
    }

    /// <summary>
    /// 株価データのリストを保持するクラス
    /// </summary>
    public class PriceList : IEnumerable<Price>
    {
        private readonly List<Price> _priceList;

        /// <summary>
        /// タイムフレームを取得する。
        /// </summary>
        public TimeFrame TimeFrame { get; private set; }

        /// <summary>
        /// 最初の株価データを取得する。
        /// </summary>
        public Price First
        {
            get { return _priceList.Count > 0 ? _priceList[0] : null; }
        }

        /// <summary>
        /// 最後の株価データを取得する。
        /// </summary>
        public Price Last
        {
            get { return _priceList.Count > 0 ? _priceList[_priceList.Count - 1] : null; }
        }

        /// <summary>
        /// リストの要素数を返す。
        /// </summary>
        public int Count
        {
            get { return _priceList.Count; }
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="prices">株価データのリスト</param>
        /// <param name="timeFrame">タイムフレーム</param>
        public PriceList(List<Price> prices, TimeFrame timeFrame)
        {
            _priceList = prices;
            TimeFrame = timeFrame;
        }

        /// <summary>
        /// PriceListを反復処理する列挙子を返す。
        /// </summary>
        /// <returns>列挙子</returns>
        public IEnumerator<Price> GetEnumerator()
        {
            return _priceList.GetEnumerator();
        }

        /// <summary>
        /// PriceListを反復処理する列挙子を返す。
        /// </summary>
        /// <returns>列挙子</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return _priceList.GetEnumerator();
        }

        /// <summary>
        /// 指定したインデックスにある要素を取得する。
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public Price this[int index]
        {
            get { return _priceList[index]; }
        }

        /// <summary>
        /// 指定した日付を持つ株価データのインデックスを返す。
        /// </summary>
        /// <param name="date">日付</param>
        /// <returns>見付かった場合はインデックスを見付からなかった場合は負の数を返す。</returns>
        public int SearchByDate(DateTime date)
        {
            return _priceList.BinarySearch(new Price {Date = date}, new PriceComparer());
        }

        private class PriceComparer : IComparer<Price>
        {
            public int Compare(Price a, Price b)
            {
                return a.Date.CompareTo(b.Date);
            }
        }
    }

    /// <summary>
    /// 価格データを操作するクラス
    /// </summary>
    public class PriceData
    {
        /// <summary>
        /// 価格データの中でもっとも大きな日付を取得または設定する。
        /// </summary>
        public static DateTime MaxDate
        {
            get
            {
                var f = Path.Combine(Global.DirPrice, "MaxDate");
                try
                {
                    var s = new FileStream(f, FileMode.OpenOrCreate);
                    using (var b = new BinaryReader(s))
                        return new DateTime((long)b.ReadInt32() * 86400 * 10000000);
                }
                catch (DirectoryNotFoundException)
                {
                    return DateTime.MinValue;
                }
                catch (EndOfStreamException)
                {
                    return DateTime.MinValue;
                }
            }
            set
            {
                var f = Path.Combine(Global.DirPrice, "MaxDate");
                var s = new FileStream(f, FileMode.OpenOrCreate);
                using (var b = new BinaryWriter(s))
                    b.Write((int)(value.Ticks / 86400 / 10000000));
            }
        }

        private static string PricePath(string code)
        {
            var dir = Path.Combine(Global.DirPrice, code.Substring(0, 1));
            if (!Directory.Exists(dir))
                Directory.CreateDirectory(dir);
            return Path.Combine(dir, code);
        }

        /// <summary>
        /// 価格データを読み出す。
        /// </summary>
        /// <param name="code">証券コード</param>
        /// <param name="timeFrame">タイムフレーム</param>
        /// <param name="needLastWeek">終わっていない週足を返すか</param>
        public static PriceList GetPrices(string code, TimeFrame timeFrame, bool needLastWeek = false)
        {
            var file = PricePath(code);
            if (!File.Exists(file))
                return null;
            var prices = new List<Price>();
            using (var s = new FileStream(file, FileMode.Open))
                try
                {
                    var buf = new byte[s.Length];
                    s.Read(buf, 0, (int)s.Length);
                    var b = new BinaryReader(new MemoryStream(buf));
                    while (true)
                    {
                        var p = new Price {Code = code};
                        p.Read(b);
                        prices.Add(p);
                    }
                }
                catch (EndOfStreamException)
                {
                }
            foreach (var split in GlobalEnv.BrandData[code].Split)
            {
                if (split.Date > prices[prices.Count - 1].Date)
                    continue;
                foreach (var price in prices)
                    if (price.Date < split.Date)
                        price.Split(split.Ratio);
                    else
                        break;
            }
            return timeFrame == TimeFrame.Daily
                       ? new PriceList(prices, TimeFrame.Daily)
                       : GenerateWeeklyPrices(prices, needLastWeek);
        }

        private static PriceList GenerateWeeklyPrices(List<Price> daily, bool needLastWeek)
        {
            if (daily.Count == 0) // 空のデータファイルが存在する場合。
                return new PriceList(daily, TimeFrame.Weekly);
            var weekly = new List<Price>();
            var code = daily[0].Code;
            var date = daily[0].Date;
            var open = 0;
            var high = 0;
            var low = 0;
            var close = 0;
            double volume = 0;
            var prevDow = DayOfWeek.Sunday;
            foreach (var d in daily)
            {
                if (prevDow > d.Date.DayOfWeek)
                {
                    var w = new Price
                        {
                            Code = code,
                            Date = date,
                            Open = open,
                            High = high,
                            Low = low,
                            Close = close,
                            Volume = volume
                        };
                    weekly.Add(w);
                    //次の週のデータを用意する。
                    date = d.Date;
                    open = d.Open;
                    high = d.High;
                    low = d.Low;
                    close = d.Close;
                    volume = d.Volume;
                }
                else
                {
                    if (d.High > high)
                        high = d.High;
                    if (low == 0 || (d.Low > 0 && d.Low < low))
                        low = d.Low;
                    if (open == 0) // 最初に付いた値段が始値
                        open = d.Open;
                    if (d.Close != 0)
                        close = d.Close;
                    volume += d.Volume;
                }
                prevDow = d.Date.DayOfWeek;
            }

            // 週の最終営業日か、終わっていない週足を返す場合は最後の週足を加える。
            if (Calendar.IsLastOpenDateOfWeek(daily[daily.Count - 1].Date) || needLastWeek)
            {
                var w = new Price
                    {
                        Code = code,
                        Date = date,
                        Open = open,
                        High = high,
                        Low = low,
                        Close = close,
                        Volume = volume
                    };
                weekly.Add(w);
            }
            return new PriceList(weekly, TimeFrame.Weekly);
        }

        private static readonly Dictionary<string, FileStream> OpenFiles = new Dictionary<string, FileStream>();

        /// <summary>
        /// 価格データを追加する。
        /// </summary>
        /// <param name="price">Priceオブジェクト</param>
        /// <param name="close">ファイルを閉じるか</param>
        public static void Add(Price price, bool close)
        {
            FileStream s;
            if (!OpenFiles.TryGetValue(price.Code, out s))
            {
                var file = PricePath(price.Code);
                s = new FileStream(file, FileMode.OpenOrCreate);
                OpenFiles.Add(price.Code, s);
            }
            if (s.Length >= Price.RecordSize)
            {
                s.Seek(-1 * Price.RecordSize, SeekOrigin.End);
                var last = new Price();
                last.Read(new BinaryReader(s));
                if (price.Date <= last.Date) // すでに存在する。
                    goto Exit;
            }
            price.Write(new BinaryWriter(s));
            Exit:
            if (!close)
                return;
            s.Close();
            OpenFiles.Remove(price.Code);
        }

        /// <summary>
        /// ファイルをすべて閉じる。
        /// </summary>
        public static void CloseAll()
        {
            foreach (var s in OpenFiles.Values)
                s.Close();
            OpenFiles.Clear();
        }

        /// <summary>
        /// 指定された証券コードの最後に更新された日付を返す。
        /// </summary>
        /// <param name="code">証券コード</param>
        /// <returns>日付</returns>
        public static DateTime MaxDateByCode(string code)
        {
            var file = PricePath(code);
            try
            {
                using (var s = new FileStream(file, FileMode.Open))
                {
                    s.Seek(-1 * Price.RecordSize, SeekOrigin.End);
                    var r = new Price();
                    r.Read(new BinaryReader(s));
                    return r.Date;
                }
            }
            catch (IOException)
            {
                return DateTime.MinValue;
            }
        }

        /// <summary>
        /// 指定された日付以降の価格データを削除する。
        /// </summary>
        /// <param name="since">日付</param>
        public static void Delete(DateTime since)
        {
            foreach (var file in CollectFiles(""))
            {
                var s = File.Open(file, FileMode.Open);
                s.Seek(0, SeekOrigin.End);
                var r = new Price();
                try
                {
                    s.Seek(-1 * Price.RecordSize, SeekOrigin.Current);
                    var b = new BinaryReader(s);
                    while (true)
                    {
                        r.Read(b);
                        if (r.Date < since)
                            break;
                        s.Seek(-2 * Price.RecordSize, SeekOrigin.Current);
                    }
                    s.SetLength(s.Seek(0, SeekOrigin.Current));
                }
                catch (IOException)
                {
                }
                finally
                {
                    s.Close();
                }
            }
            MaxDate = since.AddDays(-1);
        }

        /// <summary>
        /// 株価データをCSV形式に変換する。
        /// </summary>
        /// <param name="start">開始する銘柄コードを指定する。</param>
        /// <param name="end">終了する銘柄コードを指定する。</param>
        /// <returns>完了したらtrueを中断したらfalseを返す。</returns>
        public static bool ConvertToCSV(string start, string end)
        {
            var overwriteAll = false;
            foreach (var file in CollectFiles(""))
            {
                var code = Path.GetFileName(file);
                if (String.Compare(code, start, StringComparison.Ordinal) < 0 ||
                    String.Compare(code, end, StringComparison.Ordinal) > 0)
                    continue;
                var prices = GetPrices(code, TimeFrame.Daily);
                if (prices == null)
                    continue;
                if (!overwriteAll && File.Exists(file + ".csv"))
                {
                    using (var dialog = new OverwriteDialog())
                    {
                        dialog.Text = "株価データの保存";
                        dialog.File = file + ".csv";
                        var result = dialog.ShowDialog();
                        if (result == DialogResult.Cancel)
                            return false;
                        if (result == DialogResult.No)
                            continue;
                        if (result == DialogResult.OK)
                            overwriteAll = true;
                    }
                }
                FileStream f;
                Retry:
                try
                {
                    f = new FileStream(file + ".csv", FileMode.Create);
                }
                catch (IOException e)
                {
                    var result = MessageBox.Show(e.Message, "株価データの変換", MessageBoxButtons.AbortRetryIgnore);
                    if (result == DialogResult.Abort)
                        return false;
                    if (result == DialogResult.Ignore)
                        continue;
                    goto Retry;
                }
                using (var txt = new StreamWriter(f, Encoding.ASCII))
                {
                    foreach (var p in prices)
                        txt.WriteLine(string.Join(",", new[]
                            {
                                p.Date.ToString("d"), p.Open.ToString(),
                                p.High.ToString(), p.Low.ToString(), p.Close.ToString(), p.Volume.ToString()
                            }));
                }
            }
            return true;
        }

        /// <summary>
        /// CVS形式から株価データに変換する。
        /// </summary>
        /// <param name="start">開始する銘柄コードを指定する。</param>
        /// <param name="end">終了する銘柄コードを指定する。</param>
        /// <returns>完了したらtrueを中断したらfalseを返す。</returns>
        public static bool ConvertFromCSV(string start, string end)
        {
            var overwriteAll = false;
            foreach (var file in CollectFiles(".csv"))
            {
                var code = Path.GetFileNameWithoutExtension(file);
                if (String.Compare(code, start, StringComparison.Ordinal) < 0 ||
                    String.Compare(code, end, StringComparison.Ordinal) > 0)
                    continue;
                var prices = new List<Price>();
                Retry:
                var num = 0;
                try
                {
                    using (var f = new StreamReader(file, Encoding.ASCII))
                    {
                        string line;
                        while ((line = f.ReadLine()) != null)
                        {
                            num++;
                            var token = line.Split(',');
                            var p = new Price
                                {
                                    Code = code,
                                    Date = DateTime.Parse(token[0]),
                                    Open = int.Parse(token[1]),
                                    High = int.Parse(token[2]),
                                    Low = int.Parse(token[3]),
                                    Close = int.Parse(token[4]),
                                    Volume = double.Parse(token[5])
                                };
                            prices.Add(p);
                        }
                    }
                }
                catch (FormatException)
                {
                    var result = MessageBox.Show(file + "の" + num + "行目に不正な値があります。" +
                                                 "このファイルを無視して処理を継続しますか？",
                                                 "株価データの変換", MessageBoxButtons.YesNo);
                    if (result == DialogResult.No)
                        return false;
                    continue;
                }
                catch (IOException e)
                {
                    var result = MessageBox.Show(e.Message, "株価データの変換", MessageBoxButtons.AbortRetryIgnore);
                    if (result == DialogResult.Abort)
                        return false;
                    if (result == DialogResult.Ignore)
                        continue;
                    goto Retry;
                }
                var dst = file.Substring(0, file.Length - 4);
                if (!overwriteAll && File.Exists(dst))
                {
                    using (var dialog = new OverwriteDialog())
                    {
                        dialog.Text = "株価データの変換";
                        dialog.File = dst;
                        var result = dialog.ShowDialog();
                        if (result == DialogResult.Cancel)
                            return false;
                        if (result == DialogResult.No)
                            continue;
                        if (result == DialogResult.OK)
                            overwriteAll = true;
                    }
                }
                File.Delete(dst);
                foreach (var p in prices)
                    Add(p, false);
                CloseAll();
                var max = MaxDateByCode(code);
                if (max > MaxDate)
                    MaxDate = max;
            }
            return true;
        }

        /// <summary>
        /// 指定した拡張子のファイルを株価データのディレクトリから探す。
        /// </summary>
        /// <param name="extention">拡張子を指定する。</param>
        /// <returns>ファイルのリストを返す。</returns>
        private static IEnumerable<string> CollectFiles(string extention)
        {
            var result = new List<string>();
            foreach (var dir in Directory.GetDirectories(Global.DirPrice, "*"))
                foreach (var path in Directory.GetFiles(Path.Combine(Global.DirPrice, dir), "*"))
                {
                    var ext = Path.GetExtension(path);
                    if (ext != null && String.Compare(ext, extention, StringComparison.InvariantCultureIgnoreCase) == 0)
                        result.Add(path);
                }
            return result;
        }
    }
}