using System;
using System.Collections.Generic;
using System.IO;

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="s">FileStream</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="s">FileStream</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 byte[] buffer = new byte[Price.RecordSize];
        static MemoryStream memoryStream = new MemoryStream(buffer);
        static BinaryReader binaryReader = new BinaryReader(memoryStream);
        static BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
        
        /// <summary>
        /// 価格データを追加する。
        /// </summary>
        /// <param name="price">Priceオブジェクト</param>
        static public void Add(Price price)
        {
            var file = PricePath(price.Code);
            var s = new FileStream(file, FileMode.OpenOrCreate);
            try
            {
                s.Seek(-1 * Price.RecordSize, SeekOrigin.End);
                s.Read(buffer, 0, Price.RecordSize);
                memoryStream.Seek(0, SeekOrigin.Begin);
                var last = new Price();
                last.Read(binaryReader);
                if (price.Date <= last.Date) // すでに存在する。
                {
                    s.Close();
                    return;
                }
            }
            catch (IOException)
            {}
            memoryStream.Seek(0, SeekOrigin.Begin);
            price.Write(binaryWriter);
            s.Write(buffer, 0, Price.RecordSize);
            s.Close();
        }

        /// <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);
                    s.Read(buffer, 0, Price.RecordSize);
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    var r = new Price();
                    r.Read(binaryReader);
                    return r.Date;
                }
            }
            catch (IOException)
            {
                return DateTime.MinValue;
            }
        }

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