using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace MOEX.ASTS.Client.Demo
{
    static class Program
    {
        static void Main()
        {
            // For correct operation with code pages under .NET Core an
            // additional package is required to be installed and registered.
            // see https://www.nuget.org/packages/System.Text.Encoding.CodePages/
            // see https://stackoverflow.com/questions/37870084/net-core-doesnt-know-about-windows-1252-how-to-fix
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

            Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
            Trace.AutoFlush = true;

            var parameters = new StringDictionary();
            parameters.Add("Interface", "IFCBroker_20");
            parameters.Add("Server", "INET_GATEWAY");
            parameters.Add("Service", "inet_gateway");
            parameters.Add("Broadcast", "91.208.232.101");
            parameters.Add("UserId", "MU0000800001");
            parameters.Add("Logging", "0,0");
            parameters.Add("LogFolder", ".");
            
            try {
                new Demo().Run(parameters);
            } catch (Exception x) {
                Trace.Fail(x.ToString());
            }
        }
    }
    
    class Demo : IBinder
    {
        
        public void Run(StringDictionary parameters)
        {
            // Dynamically load appropriate ASTS Bridge API (mtesrl.dll)
            var is64bit = IntPtr.Size == 8;
            var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

            Module module;
            if (isWindows)
            {
                var name = is64bit ? "win64\\mtesrl64.dll" : "win32\\mtesrl.dll";
                // TODO: Fix this path, this one is based on demo project location/configuration
                module = Module.Load("..\\..\\..\\..\\embedded\\" + name);
            }
            else
            {
                // NOTE: under Linux before running the Demo you'd first configure
                // export LD_LIBRARY_PATH="<path>:$LD_LIBRARY_PATH"
                // where <path> specifies location of the appropriate mtesrl.so
                module = Module.Load("mtesrl");
            }
            // Module version information
            Trace.WriteLine("Module: " + module.Version);

            try {
                
                // Start ASTS connection
                var client = module.Connect(parameters);
                try {
                    // Test some demo operations
                    Test(client);
                } finally {
                    // Close ASTS connection
                    client.Close();
                }

            } finally {
                // Dispose of ASTS Bridge API
                module.Dispose();
                Trace.WriteLine("====================");
                Trace.WriteLine("Done.");
            }
        }
        
        void Test(Client client)
        {
            // Some useful info about connection
            Trace.WriteLine(string.Format("Client: handle={0}, language={1}", 
                                          client.Handle, client.Language));
            var server = client.ServerInfo;
            Trace.WriteLine(string.Format("Server: SystemID={0}, SessionID={1}, UserID={2}",
                                          server.SystemID, server.SessionID, server.UserID));

            // Examine market structure
            var market = client.MarketInfo;
            Trace.WriteLine("Market: " + market.ToString());
            
            // Optional SelectBoards() to narrow data
            var boards = new HashSet<string>();
            boards.Add("TQBR");
            client.SelectBoards(boards);
            
            // Load() table - mimics a sequence of MTEOpenTable/MTECloseTable.
            // Use it for non-updateable info-requests
            var parser = client.Load("MARKETS", null);
            var rows = parser.Execute(this);
            Trace.WriteLine(string.Format("Loaded MARKETS: bytes={0}, rows={1}", parser.Length, rows));
            
            // Open() table - it will also be added to the list
            // of requests to be updated at Refresh() call
            var table = market.Tables.Find(Meta.TableType.SysTime);
            parser = client.Open(table.Name, null, true);
            rows = parser.Execute(this);
            Trace.WriteLine(string.Format("Opened {0}: bytes={1}, rows={2}", table.Name, parser.Length, rows));
            
            // Open() SECURITIES (params==null - all securities)
            table = market.Tables.Find(Meta.TableType.Securities);
            parser = client.Open(table.Name, null, true);
            rows = parser.Execute(this);
            Trace.WriteLine(string.Format("Opened {0}: bytes={1}, rows={2}", table.Name, parser.Length, rows));
            
            // Open() ORDERBOOK / EXT_ORDERBOOK (if any)
            table = market.Tables.Find(Meta.TableType.Orderbooks);
            if (table != null) {
                parser = client.Open(table.Name, null, true);
                rows = parser.Execute(this);
                Trace.WriteLine(string.Format("Opened {0}: bytes={1}, rows={2}", table.Name, parser.Length, rows));
            }
            
            
            byte[] snapshot = new byte[32];
            // make 10 Refresh() iterations with some delay between them
            for (var i = 0; i < 10; i++) {
                Thread.Sleep(1000);
                parser = client.Refresh();
                if (parser.IsEmpty) continue; // nothing to parse, skip the rest
                var bytes = parser.Length;
                var count = parser.Execute(this);
                Trace.WriteLine("====================");
                Trace.WriteLine(string.Format("Parsed {0} bytes, {1} rows", bytes, count));
            }
        }
        
        // Very simple table storage.
        readonly Dictionary<string, Table> database = new Dictionary<string, Table>();
        
        // IBinder implementation
        public ITarget Detect(Meta.Message source)
        {
            Table target;
            if (database.TryGetValue(source.Name, out target))
                return target;
            database.Add(source.Name, target = new Table());
            return target;
        }
        
        // Very simple record.
        class Record
        {
            internal int decimals;
            internal Dictionary<string, object> values = new Dictionary<string, object>();
        }
        
        // Very simple table.
        class Table : ITarget
        {
            
            // Записи, хэшированные по значению первичного ключа (если есть)
            // или по порядковому номеру при отсутствии ключевых полей.
            readonly Dictionary<string, Record> records = new Dictionary<string, Record>();
            
            // Специальная структура для хранения "стаканов" - блоки записей,
            // хешированные по коду инструмента (SECBOARD + SECCODE).
            readonly Dictionary<string, List<Record>> orderbooks = new Dictionary<string, List<Record>>();

            #region ITarget implementation

            public void InitTableUpdate(Meta.Message source, string filter)
            {
                // Парсер начинает обрабатывать буфер ответа.
                // Здесь при необходимости нужно провести
                // обработку в зависимости от комбинации значений
                // source.ClearOnUpdate, source.IsOrderbook
                if (source.ClearOnUpdate)
                    records.Clear();
            }

            private List<Record> orderbook;
            
            public void DoneTableUpdate(Meta.Message source, string filter)
            {
                // Просто окончание работы парсера, а-ля commit.
                // Очистка более ненужных переменных.
                orderbook = null;
            }

            private OrderedDictionary keys = new OrderedDictionary();
            
            private Record current;
            
            public void SetKeyValue(Meta.Field field, object value)
            {
                // Задает значения ключевых полей.
                // В демо-примере - собираем значения в dictionary.
                // По окончании обработки записи собранные значения 
                // следует сбросить (см. DoneRecordUpdate())
                keys.Add(field.Name, value);
            }

            public bool InitRecordUpdate(Meta.Message source, string filter)
            {
                // Начало обработки записи.
                // Здесь нужно произвести поиск по ключам
                // и вернуть true, если запись не найдена (т.е. новая)
                
                if (source.IsOrderbook) {
                    // Для таблиц типа "orderbook" - специальная обработка.
                    // Запись всегда будет новой, добавим ее в "стакан"
                    current = new Record();
                    orderbook.Add(current);
                    return true;
                }
                
                if (keys.Count == 0) {
                    // SetKeyValue() ни разу не был вызван -
                    // данная таблица не имеет первичных ключей.
                    // Все записи будут считаться новыми, хранить
                    // их будем по порядковому номеру.
                    current = new Record();
                    records.Add(records.Count.ToString(), current);
                    return true;
                } else {
                    // У таблицы есть первичные ключи - 
                    // ищем и храним записи по их значению.
                    var b = new StringBuilder();
                    foreach (DictionaryEntry e in keys) {
                        b.Append(e.Key.ToString())
                            .Append('=')
                            .Append(e.Value.ToString())
                            .Append(';');
                    }
                    var s = b.ToString();
                    if (records.TryGetValue(s, out current)) {
                        // Запись найдена
                        return false;
                    }
                    // Запись по ключу не найдена - создадим новую
                    current = new Record();
                    records.Add(s, current);
                    return true;
                }
            }

            public int GetRecordDecimals()
            {
                // Парсеру для работы понадобилось знать
                // о кол-ве десятичных знаков по найденной 
                // ранее записи - отдать сохраненное.
                return current.decimals;
            }

            public void SetRecordDecimals(int decimals)
            {
                // Парсер дли новой записи определил 
                // кол-во десятичных знаков - сохранить (в записи).
                current.decimals = decimals;
            }

            public void SetFieldValue(Meta.Field field, object value)
            {
                // Парсер задает значение конкретного поля записи
                current.values[field.Name] = value;
            }

            public void DoneRecordUpdate(Meta.Message source, string filter)
            {
                // Закончили обрабатывать запись.
                // Хорошее место для сохранения накопленных данных куда-то.
                // В демо-примере - просто печать в консоль.
//                var b = new StringBuilder();
//                foreach (var e in current.values)
//                    b.Append(e.Key).Append('=').Append(Convert.ToString(e.Value)).Append(';');
//                Trace.WriteLine("Table: " + source.Name + "; record: " + b.ToString());
                
                // Здесь также следует сбросить собранные значения первичных ключей,
                // чтобы подготовиться к получению значений для следующей записи.
                keys.Clear();
                current = null;
            }

            public void SwitchOrderbook(Meta.Message source, string filter, string board, string paper)
            {
                // Специфическая операция для таблиц типа "котировки" (table.isOrderbook())
                // Для таких таблиц значения ключевых полей setKeyField() не задаются для 
                // каждой записи, вместо этого идет "переключение стакана" - блока записей.
                // Данный вызов информирует вас о том, что начинается новый блок
                // для указанного инструмента ticker.
                if (orderbooks.TryGetValue(board + '.' + paper, out orderbook)) {
                    // "Стакан" уже есть - его нужно очистить,
                    // т.к. новые значения полностью заменяют старые.
                    orderbook.Clear();
                } else {
                    // Инструмент встретился впервые,
                    // подготовим для него "стакан"
                    orderbook = new List<Record>();
                    orderbooks.Add(board + '.' + paper, orderbook);
                }
            }

            #endregion
        }
        
    }
    
}
