// Copyright (C) 2010, 2012 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: BrandData.cs 434 2012-02-06 22:50:34Z panacoran $

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

namespace Protra.Lib.Data
{
    /// <summary>
    /// 分割情報を格納するクラス。
    /// </summary>
    public class Split
    {
        /// <summary>
        /// 基準日。
        /// </summary>
        public DateTime Date { set; get; }
        /// <summary>
        /// 分割比率。
        /// </summary>
        public double Ratio { set; get; }
    }

    /// <summary>
    /// 市場コードを市場名に変換するクラス
    /// </summary>
    public class Market
    {
        /// <summary>
        /// 市場コードを市場名に変換する。
        /// </summary>
        /// <param name="code">市場コード</param>
        /// <returns>市場名</returns>
        public static string Name(string code)
        {
            switch (code)
            {
                case "T1":
                    return "東証1部";
                case "T2":
                    return "東証2部";
                case "M":
                    return "マザーズ";
                case "O1":
                    return "大証1部";
                case "O2":
                    return "大証2部";
                case "H":
                    return "ヘラクレス";
                case "J":
                    return "JASDAQ";
                default:
                    return "不明";
            }
        }

        /// <summary>
        /// 市場コード一覧を取得する。
        /// </summary>
        public static string[] Codes
        {
            get
            {
                return new string[] { "T1", "T2", "M", "O1", "O2", "H", "J" };
            }
        }

        /// <summary>
        /// 市場名一覧を取得する。
        /// </summary>
        public static string[] Names
        {
            get
            {
                string[] names = new string[Codes.Length];
                for (int i = 0; i < Codes.Length; i++)
                    names[i] = Name(Codes[i]);
                return names;
            }
        }
    }

    /// <summary>
    /// 銘柄データを格納するクラス。
    /// </summary>
    public class Brand : IComparable<Brand>
    {
        /// <summary>
        /// 銘柄のフラグ
        /// </summary>
        public enum Flag
        {
            /// <summary>
            /// 上場廃止。
            /// </summary>
            OBS = 1,
            /// <summary>
            /// 日経平均採用銘柄。
            /// </summary>
            N255 = 2,
            /// <summary>
            /// 売買代金上位500位。
            /// </summary>
            A500 = 4
        }
        /// <summary>
        /// 証券コードを取得または設定する。
        /// </summary>
        public string Code { get; set; }
        /// <summary>
        /// 銘柄名を取得または設定する。
        /// </summary>
        public string Name { get;  set; }
        /// <summary>
        /// 市場コードを取得または設定する。
        /// </summary>
        public string Market { get; set; }
        /// <summary>
        /// 単元を取得または設定する。
        /// </summary>
        public int Unit { get; set; }
        /// <summary>
        /// フラグを取得または設定する。
        /// </summary>
        public Flag Flags { set; get; }
        /// <summary>
        /// 分割情報を取得または設定する。
        /// </summary>
        public List<Split> Split { set; get; }

        /// <summary>
        /// 市場名を取得する。
        /// </summary>
        /// <returns>市場名</returns>
        public string MarketName
        {
            get
            {
                return Data.Market.Name(Market);
            }
        }

        /// <summary>
        /// コンストラクタ。
        /// </summary>
        public Brand()
        {
            Split = new List<Split>();
        }


        /// <summary>
        /// このインスタンスと指定したBrandオブジェクトを比較する。
        /// </summary>
        /// <param name="other">Brandオブジェクト</param>
        /// <returns>比較結果</returns>
        public int CompareTo(Brand other)
        {
                return Code.CompareTo(other.Code);
        }

        /// <summary>
        /// 文字列に変換する。
        /// </summary>
        /// <returns>文字列</returns>
        public override string ToString()
        {
            return Code + " " + MarketName + " " + Name;
        }
    }

    /// <summary>
    /// 銘柄データを管理するクラス。
    /// </summary>
    public class BrandData
    {
        Dictionary<string, Brand> data;

        /// <summary>
        /// index.txtが更新されたかどうかを取得または設定する。
        /// </summary>
        public bool Updated { get; private set; }

        private static Brand unknown = new Brand();

        private FileSystemWatcher watcher = new FileSystemWatcher(Global.DirData);
        private MethodInvoker handler;

        /// <summary>
        /// ファイルの作成を監視する設定をする。
        /// </summary>
        /// <param name="handler">ファイルが作成されたときに呼ばれるハンドラ</param>
        public void SetWatcher(MethodInvoker handler)
        {
            watcher.Filter = "index.txt";
            watcher.Changed += new FileSystemEventHandler(watcher_Created);
            watcher.EnableRaisingEvents = true;
            watcher.SynchronizingObject = (Form)handler.Target;
            this.handler = handler;
        }

        void watcher_Created(object sender, FileSystemEventArgs e)
        {
            Load();
            GlobalEnv.BrandListConfig.SetDefaultBrandList();  // A500とN225の変更を反映させる。
            handler();
        }

        /// <summary>
        /// 証券コードに対応する銘柄情報を取得する。
        /// </summary>
        /// <param name="code">証券コード</param>
        /// <returns>銘柄情報</returns>
        public Brand this[string code]
        {
            get
            {
                try
                {
                    return data[code];
                }
                catch (KeyNotFoundException)
                {
                    var unknown = new Brand();
                    unknown.Code = code;
                    unknown.Name = "不明";
                    unknown.Market = "不明";
                    unknown.Unit = 0;
                    return unknown;
                }
            }
        }

        /// <summary>
        /// 日経平均採用銘柄を返す。
        /// </summary>
        /// <returns>証券コードのリスト</returns>
        public List<string> Nikkei225()
        {
            var brands = new List<string>();
            foreach (var b in data.Values)
                if ((b.Flags & Brand.Flag.N255) != 0)
                    brands.Add(b.Code);
            return brands;
        }

        /// <summary>
        /// 売買代金上位500位の銘柄を返す。
        /// </summary>
        /// <returns>証券コードのリスト</returns>
        public List<string> A500()
        {
            var brands = new List<string>();
            foreach (var b in data.Values)
                if ((b.Flags & Brand.Flag.A500) != 0)
                    brands.Add(b.Code);
            return brands;
        }

        /// <summary>
        /// 必要ならindex.txtを更新して銘柄情報を読み込む。
        /// </summary>
        public void Load()
        {
            watcher.EnableRaisingEvents = false;
            data = new Dictionary<string, Brand>();
            var b = new Brand();
            b.Code = "1001";
            b.Market = "T1";
            b.Name = "日経平均";
            b.Unit = 1;
            data[b.Code] = b;
            b = new Brand();
            b.Code = "1002";
            b.Market = "T1";
            b.Name = "ＴＯＰＩＸ";
            b.Unit = 1;
            data[b.Code] = b;
            try
            {
                using (var reader = new StreamReader(OpenIndex(), Encoding.GetEncoding("shift_jis")))
                {
                    reader.ReadLine(); // @dateを読み飛ばす。
                    string line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        var brand = new Brand();
                        string[] entries = line.Split(',');
                        brand.Code = entries[0];
                        brand.Name = entries[1].Replace("ホールディングス", "ＨＤ");
                        brand.Market = entries[2];
                        brand.Unit = int.Parse(entries[3]);
                        for (int i = 4; i < entries.Length; i++)
                        {
                            if (entries[i] == "OBS")
                                brand.Flags |= Brand.Flag.OBS;
                            else if (entries[i] == "N225")
                                brand.Flags |= Brand.Flag.N255;
                            else if (entries[i] == "A500")
                                brand.Flags |= Brand.Flag.A500;
                            else if (!entries[i].StartsWith("S:"))
                                throw new ApplicationException("index.txtが不正です。:\n" + line);
                            else
                            {
                                // 分割比率を処理。
                                var split = new Split();
                                int y = int.Parse(entries[i].Substring(2, 4));
                                int m = int.Parse(entries[i].Substring(6, 2));
                                int d = int.Parse(entries[i].Substring(8, 2));
                                split.Date = new DateTime(y, m, d);
                                split.Ratio = double.Parse(entries[i].Substring(11));
                                brand.Split.Add(split);
                            }
                        }
                        data.Add(brand.Code, brand);
                    }
                }
            }
            catch (Exception e)
            {
                using (var dialog = new ApplicationError())
                {
                    dialog.ErrorMessage = e.Message;
                    dialog.Mode = ApplicationError.ErrorType.Fatal;
                    dialog.ShowDialog();
                    Environment.Exit(1);
                }
            }
            watcher.EnableRaisingEvents = true;
        }

        private Stream OpenIndex()
        {
            Updated = false;
            var name = Path.Combine(Global.DirData, "index.txt");
            var dl = new DownloadUtil();
            dl.Url = "http://protra.sourceforge.jp/data/index.txt.lzh";
            try
            {
                dl.IfModifiedSince = File.GetLastWriteTime(name);
            }
            catch
            {
                dl.IfModifiedSince = DateTime.MinValue;
            }
            using (var stream = dl.DownloadAndExtract())
            {
                if (stream == null)
                    return File.OpenRead(name);
                Updated = true;
                var file = File.Create(name);
                var n = 0;
                var buf = new byte[4096];
                while ((n = stream.Read(buf, 0, 4096)) > 0)
                    file.Write(buf, 0, n);
                file.Seek(0, SeekOrigin.Begin);
                return file;
            }
        }

        /// <summary>
        /// 指定された文字列を名前に含む銘柄データを返す。
        /// </summary>
        /// <param name="name">文字列</param>
        /// <returns>銘柄データ</returns>
        public List<Brand> Search(string name)
        {
            var r = new List<Brand>();
            foreach (Brand b in data.Values)
            {
                var c = CompareInfo.GetCompareInfo("ja-JP");
                if (c.IndexOf(b.Name, name,
                    CompareOptions.IgnoreCase | CompareOptions.IgnoreWidth) != -1)
                    r.Add(b);
            }
            r.Sort();
            return r;
        }

        /// <summary>
        /// 証券コードに対応する銘柄情報が存在するかどうか判断します。
        /// </summary>
        /// <param name="code">証券コード</param>
        /// <returns>銘柄情報が存在するかどうかを示すブール値</returns>
        public bool Contains(string code)
        {
            return data.ContainsKey(code);
        }
    }
}
