// Copyright(C) 2010 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$

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

namespace Protra.Lib.Data
{
    /// <summary>
    /// 株価データをあらわすクラス。
    /// </summary>
    public class Price
    {
        /// <summary>
        /// レコードサイズ
        /// </summary>
        static public 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 class PriceData
    {
        /// <summary>
        /// 価格データの中でもっとも大きな日付を取得または設定する。
        /// </summary>
        static public DateTime MaxDate {
            get
            {
                var f = Path.Combine(Global.DirPrice, "MaxDate");
                var s = new FileStream(f, FileMode.OpenOrCreate);
                using (var b = new BinaryReader(s))
                    try
                    {
                        return new DateTime((long)b.ReadInt32() * 86400 * 10000000);
                    }
                    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));
            }
        }

        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>
        static public List<Price> Prices(string code)
        {
            var prices = new List<Price>();
            var file = PricePath(code);
            if (!File.Exists(file))
                return prices;
            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();
                        p.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 (Price price in prices)
                    if (price.Date < split.Date)
                        price.Split(split.Ratio);
                    else
                        break;
            }
            return prices;
        }

        /// <summary>
        /// 週足の価格データを作成して返す。
        /// </summary>
        /// <param name="code">証券コード</param>
        /// <param name="needLastWeek">終わっていない週足を返すか</param>
        /// <returns></returns>
        static public List<Price> WeeklyPrices(string code, bool needLastWeek)
        {
            var daily = Prices(code);
            if (daily.Count == 0)
                return daily;
            var weekly = new List<Price>();
            DateTime date = daily[0].Date;
            int open = 0; int high = 0; int low = 0; int close = 0;
            double volume = 0;
            var prev_dow = DayOfWeek.Sunday;
            foreach (var d in daily)
            {
                if (prev_dow > d.Date.DayOfWeek)
                {
                    var w = new Price();
                    w.Code = code;
                    w.Date = date;
                    w.Open = open; w.High = high; w.Low = low; w.Close = close;
                    w.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;
                }
                prev_dow = d.Date.DayOfWeek;
            }

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

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

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

        /// <summary>
        /// 指定された証券コードの最後に更新された日付を返す。
        /// </summary>
        /// <param name="code">証券コード</param>
        /// <returns>日付</returns>
        static public 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>
        static public 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)
                {
                    continue;
                }
                finally
                {
                    s.Close();
                }
            }
            MaxDate = since.AddDays(-1);
        }


        static OverwriteDialog overwriteDialog = new OverwriteDialog();

        /// <summary>
        /// 株価データをCSV形式に変換する。
        /// </summary>
        /// <param name="start">開始する銘柄コードを指定する。</param>
        /// <param name="end">終了する銘柄コードを指定する。</param>
        /// <returns>完了したらtrueを中断したらfalseを返す。</returns>
        public static bool ConvertToCSV(string start, string end)
        {
            bool overwriteAll = false;
            foreach (var file in CollectFiles(""))
            {
                var code = Path.GetFileName(file);
                if (code.CompareTo(start) < 0 || code.CompareTo(end) > 0)
                    continue;
                var prices = PriceData.Prices(code);
                if (prices == null)
                    continue;
                if (!overwriteAll && File.Exists(file + ".csv"))
                {
                    overwriteDialog.Text = "株価データの保存";
                    overwriteDialog.File = file + ".csv";
                    var result = overwriteDialog.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, System.Text.Encoding.ASCII))
                {
                    foreach (var p in prices)
                        txt.WriteLine(string.Join(",", new String[] { 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)
        {
            bool overwriteAll = false;
            foreach (var file in CollectFiles(".csv"))
            {
                var code = Path.GetFileNameWithoutExtension(file);
                if (code.CompareTo(start) < 0 || code.CompareTo(end) > 0)
                    continue;
                var prices = new List<Price>();
            Retry:
                int num = 0;
                try
                {
                    using (var f = new StreamReader(file, System.Text.Encoding.ASCII))
                    {
                        string line;
                        while ((line = f.ReadLine()) != null)
                        {
                            num++;
                            string[] 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))
                {
                    overwriteDialog.Text = "株価データの変換";
                    overwriteDialog.File = dst;
                    var result = overwriteDialog.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)
                    PriceData.Add(p, false);
                CloseAll();
                var max = MaxDateByCode(code);
                if (max > MaxDate)
                    MaxDate = max;
            }
            return true;
        }

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