// Copyright (C) 2010, 2012, 2013 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 447 2013-05-25 13:37:47Z panacoran $

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Windows.Forms;
using Protra.Lib.Config;
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[] {"T1", "T2", "M", "O1", "O2", "H", "J"}; }
        }

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

    /// <summary>
    /// 銘柄データを格納するクラス。
    /// </summary>
    public class Brand : IComparable<Brand>
    {
        /// <summary>
        /// 銘柄のフラグ
        /// </summary>
        [Flags]
        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 String.Compare(Code, other.Code, StringComparison.Ordinal);
        }

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

    /// <summary>
    /// 銘柄データを管理するクラス。
    /// </summary>
    public class BrandData
    {
        private Dictionary<string, Brand> _data;
        private readonly FileChangeWatcher _watcher;
        private readonly String _dataFile = Path.Combine(Global.DirData, "index.txt");

        /// <summary>
        /// 銘柄リストの表示を更新するデリゲートを取得または設定する。
        /// </summary>
        public MethodInvoker BrandListInit { private get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public BrandData()
        {
            if (!Directory.Exists(Global.DirData)) // DirDataは必ず存在するはずだが念のため。
                Directory.CreateDirectory(Global.DirData);
            _watcher = new FileChangeWatcher(_dataFile) {Enabled = true, ProcessFile = LoadAndUpdateBrandList};
        }

        /// <summary>
        /// 証券コードに対応する銘柄情報を取得する。
        /// </summary>
        /// <param name="code">証券コード</param>
        /// <returns>銘柄情報</returns>
        public Brand this[string code]
        {
            get
            {
                try
                {
                    return _data[code];
                }
                catch (KeyNotFoundException)
                {
                    var unknown = new Brand {Code = code, Name = "不明", Market = "不明", 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>
        /// 銘柄情報を読み込む。
        /// </summary>
        public void Load()
        {
            _data = new Dictionary<string, Brand>();
            var b = new Brand {Code = "1001", Market = "T1", Name = "日経平均", Unit = 1};
            _data[b.Code] = b;
            b = new Brand {Code = "1002", Market = "T1", Name = "ＴＯＰＩＸ", Unit = 1};
            _data[b.Code] = b;
            try
            {
                using (var stream = File.OpenRead(_dataFile))
                using (var reader = new StreamReader(stream, Encoding.GetEncoding("shift_jis")))
                {
                    reader.ReadLine(); // @dateを読み飛ばす。
                    string line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        var entries = line.Split(',');
                        var brand = new Brand
                            {
                                Code = entries[0],
                                Name = entries[1].Replace("ホールディングス", "ＨＤ"),
                                Market = entries[2],
                                Unit = int.Parse(entries[3])
                            };
                        for (var i = 4; i < entries.Length; i++)
                        {
                            switch (entries[i])
                            {
                                case "OBS":
                                    brand.Flags |= Brand.Flag.OBS;
                                    break;
                                case "N225":
                                    brand.Flags |= Brand.Flag.N255;
                                    break;
                                case "A500":
                                    brand.Flags |= Brand.Flag.A500;
                                    break;
                                default:
                                    if (!entries[i].StartsWith("S:"))
                                        throw new ApplicationException("index.txtが不正です。:\n" + line);
                                    // 分割比率を処理。
                                    var split = new Split();
                                    var y = int.Parse(entries[i].Substring(2, 4));
                                    var m = int.Parse(entries[i].Substring(6, 2));
                                    var 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);
                                    break;
                            }
                        }
                        _data.Add(brand.Code, brand);
                    }
                }
            }
            catch (FileNotFoundException)
            {
                Update();
            }
            _watcher.Enabled = true;
        }

        /// <summary>
        /// 銘柄データを更新する。
        /// </summary>
        public void Update()
        {
            var dl = new DownloadUtil("http://protra.sourceforge.jp/data/index.txt.lzh");
            try
            {
                dl.IfModifiedSince = File.GetLastWriteTime(_dataFile);
            }
            catch
            {
                dl.IfModifiedSince = DateTime.MinValue;
            }
            _watcher.Enabled = false;
            using (var stream = dl.DownloadAndExtract())
            {
                if (stream == null)
                    return;
                using (var file = File.Create(_dataFile))
                {
                    int n;
                    var buf = new byte[4096];
                    while ((n = stream.Read(buf, 0, 4096)) > 0)
                        file.Write(buf, 0, n);
                }
            }
            _watcher.Enabled = true;
            LoadAndUpdateBrandList();
        }

        private void LoadAndUpdateBrandList()
        {
            Load();
            if (GlobalEnv.BrandListConfig != null)
                GlobalEnv.BrandListConfig.SetDefaultBrandList(); // N225とA500の変更を反映させる。
            if (BrandListInit != null)
            {
                var main = (Form)BrandListInit.Target;
                if (main.InvokeRequired)
                    main.Invoke(BrandListInit, null);
                else
                    BrandListInit();
            }
        }

        /// <summary>
        /// 指定された文字列を名前に含む銘柄データを返す。
        /// </summary>
        /// <param name="name">文字列</param>
        /// <returns>銘柄データ</returns>
        public List<Brand> Search(string name)
        {
            var r = new List<Brand>();
            foreach (var 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);
        }
    }
}