// Created with MonoDevelop
//
//    Copyright (C) 2010 Hiperscan GmbH Dresden, Germany
//
//    This program 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/>.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;

using Hiperscan.SGS.Common;
using Hiperscan.Unix;

using Version = Hiperscan.SGS.Common.Version;


namespace Hiperscan.SGS.IO
{

    public static class LUT
    {
        
        private struct ParserHFormat
        {
            public const string LutOffset = @"const long lut_offset ="; 
            public const string DateTime  = @"#define LUT_VERSION ";
        }
        
        private struct Parser
        {
            public const string Culture           = @"CULTURE";
            public const string Timestamp         = @"TIMESTAMP";
            public const string AcquisitionOffset = @"ACQUISITION_OFFSET";
            public const string LutOffset         = @"LUT_OFFSET";
            public const string MsaFactor         = @"MSA_FACTOR";
            public const string FirmwareVersion   = @"FIRMWARE_VERSION";
            public const string ScanEngineVersion = @"SCAN_ENGINE_VERSION";
            public const string DateTimeFormat    = @"yyyy-MM-dd_HH:mm:ss";

            // additional fields, needed for SGS-NT
            public const string AcquisitionChannelMask = @"ACQUISITION_CHANNEL_MASK";
            public const string SkipCount              = @"SKIP_COUNT";
            public const string MinWavelength          = @"MIN_WAVELENGTH";
            public const string MaxWavelength          = @"MAX_WAVELENGTH";
            public const string Increment              = @"INCREMENT";
            public const string ExtensionRange         = @"EXTENSION_RANGE";
            public const string DarkIntensityEndpoint  = @"DARK_INTENSITY_ENDPOINT";
        }
        
        private static CultureInfo CultureInfo = new CultureInfo("en-US");

        
        public static void Write(Spectrometer.LUT lut, Uri uri, CultureInfo culture)
        {
            LUT.Write(lut, Uri.UnescapeDataString(uri.AbsolutePath), culture);
        }
        
        public static void Write(Spectrometer.LUT lut, string fname, CultureInfo culture)
        {
            if (Path.GetExtension(fname).ToLower() != ".lut")
                fname = Path.ChangeExtension(fname, ".lut");
                                               
            using (var stream = new FileIO.FileStreamWriter(fname))
            {
                stream.WriteLine("# {0}={1}",     Parser.Culture, culture.ToString());
                stream.WriteLine("# {0}={1}",     Parser.Timestamp, lut.Timestamp.ToString(Parser.DateTimeFormat));
                stream.WriteLine("# {0}={1}",     Parser.LutOffset, lut.Offset);
                stream.WriteLine("# {0}={1}",     Parser.AcquisitionOffset, lut.AcquisitionOffset);
                stream.WriteLine("# {0}={1}",     Parser.MsaFactor, lut.MsaFactor);
                stream.WriteLine("# {0}={1}.{2}", Parser.FirmwareVersion, lut.FirmwareVersion.Major, lut.FirmwareVersion.Minor);
                stream.WriteLine("# {0}={1}.{2}", Parser.ScanEngineVersion, lut.ScanEngineVersion.Major, lut.ScanEngineVersion.Minor);

                if (lut.IsExtended)
                {
                    stream.WriteLine("# {0}={1}", Parser.AcquisitionChannelMask, lut.AcquisitionChannelMask);
                    stream.WriteLine("# {0}={1}", Parser.SkipCount, lut.SkipCount);
                    stream.WriteLine("# {0}={1}", Parser.MinWavelength, lut.MinWavelength);
                    stream.WriteLine("# {0}={1}", Parser.MaxWavelength, lut.MaxWavelength);
                    stream.WriteLine("# {0}={1}", Parser.Increment, lut.Increment);
                    stream.WriteLine("# {0}={1}", Parser.ExtensionRange, lut.ExtensionRange);
                    stream.WriteLine("# {0}={1}", Parser.DarkIntensityEndpoint, lut.DarkIntensityEndpoint);
                }

                for (int ix=0; ix < lut.Data.Length; ++ix)
                {
                    stream.WriteLine(lut.Data[ix].ToString(culture));
                }
            }
        }
        
        public static Spectrometer.LUT Read(Uri uri)
        {
            return LUT.Read(Uri.UnescapeDataString(uri.AbsolutePath));
        }

        public static Spectrometer.LUT Read(string fname)
        {
            if (Path.GetExtension(fname).ToLower() == ".h")
                return LUT.ReadHFormat(fname);
            
            List<float> lut_data = new List<float>();
            
            uint lut_offset = 0;
            int  acquisition_offset = 0;
            double msa_factor = double.NaN;
            DateTime timestamp = DateTime.MinValue;
            Version firmware    = new Version(0);
            Version scan_engine = new Version(0);

            bool extended_lut = false;
            byte acquisition_channel = 0;
            uint skip_count = 0;
            float min_wavelength = float.MinValue;
            float max_wavelength = float.MaxValue;
            float increment = 1f;
            float extension_range = 0f;
            uint dark_intensity_endpoint = 0;

            using (StreamReader stream = FileIO.OpenTextFile(fname))
            {
                CultureInfo culture = LUT.CultureInfo;
                string line;
                int skipped = 0;
                int line_number = 0;
                float last = float.NaN;

                bool increasing = false;
                bool decreasing = false;
                
                while ((line = stream.ReadLine()) != null)
                {
                    ++line_number;
                    
                    string s = line.Trim();
                    try
                    {
                        float f = float.Parse(s,
                                              NumberStyles.AllowDecimalPoint|
                                              NumberStyles.AllowExponent,
                                              culture);

                        if (float.IsNaN(last) == false)
                        {
                            decreasing |= f <= last;
                            increasing |= f >= last;
                        }
                        
                        if (float.IsNaN(f))
                            throw new InvalidDataException(Catalog.GetString("A LUT must not contain NaNs."));
                        
                        last = f;
                        lut_data.Add(f);
                    }
                    catch (InvalidDataException)
                    {
                        throw;
                    }
                    catch (Exception ex)
                    {
                        ++skipped;

                        try
                        {
                            culture = (CultureInfo)LUT.ParseLine(line, LUT.Parser.Culture, culture);
                            continue;
                        } 
                        catch (InvalidDataException) {}
                        
                        try
                        {
                            timestamp = (DateTime)LUT.ParseLine(line, LUT.Parser.Timestamp, culture);
                            continue;
                        } 
                        catch (InvalidDataException) {}
                        
                        try
                        {
                            lut_offset = (uint)LUT.ParseLine(line, LUT.Parser.LutOffset, culture);
                            continue;
                        } 
                        catch (InvalidDataException) {}
                        
                        try
                        {
                            acquisition_offset = (int)LUT.ParseLine(line, LUT.Parser.AcquisitionOffset, culture);
                            continue;
                        } 
                        catch (InvalidDataException) {}

                        try
                        {
                            msa_factor = (double)LUT.ParseLine(line, LUT.Parser.MsaFactor, culture);
                            continue;
                        }
                        catch (InvalidDataException) {}
                        
                        try
                        {
                            firmware = (Version)LUT.ParseLine(line, LUT.Parser.FirmwareVersion, culture);
                            continue;
                        } 
                        catch (InvalidDataException) { }
                        
                        try
                        {
                            scan_engine = (Version)LUT.ParseLine(line, LUT.Parser.ScanEngineVersion, culture);
                            continue;
                        } 
                        catch (InvalidDataException) { }

                        try
                        {
                            acquisition_channel = (byte)LUT.ParseLine(line, LUT.Parser.AcquisitionChannelMask, culture);
                            extended_lut = true;
                            continue;
                        }
                        catch (InvalidDataException) { }

                        try
                        {
                            skip_count = (uint)LUT.ParseLine(line, LUT.Parser.SkipCount, culture);
                            extended_lut = true;
                            continue;
                        }
                        catch (InvalidDataException) { }

                        try
                        {
                            min_wavelength = (float)LUT.ParseLine(line, LUT.Parser.MinWavelength, culture);
                            extended_lut = true;
                            continue;
                        }
                        catch (InvalidDataException) { }

                        try
                        {
                            max_wavelength = (float)LUT.ParseLine(line, LUT.Parser.MaxWavelength, culture);
                            extended_lut = true;
                            continue;
                        }
                        catch (InvalidDataException) { }

                        try
                        {
                            increment = (float)LUT.ParseLine(line, LUT.Parser.Increment, culture);
                            extended_lut = true;
                            continue;
                        }
                        catch (InvalidDataException) { }

                        try
                        {
                            extension_range = (float)LUT.ParseLine(line, LUT.Parser.ExtensionRange, culture);
                            extended_lut = true;
                            continue;
                        }
                        catch (InvalidDataException) { }

                        try
                        {
                            dark_intensity_endpoint = (uint)LUT.ParseLine(line, LUT.Parser.DarkIntensityEndpoint, culture);
                            extended_lut = true;
                            continue;
                        }
                        catch (InvalidDataException) { }

                        throw new InvalidDataException(string.Format(Catalog.GetString("Unknown parameter name in line: {0}"), line), ex);
                    }

                    if (increasing && decreasing)
                        throw new InvalidDataException(Catalog.GetString("A LUT must increase or decrease monotonically."));
                }
            }
            
            Console.WriteLine("Timestamp: {0}",  timestamp.ToString());
            Console.WriteLine("LUT offset: {0}", lut_offset);
            Console.WriteLine("Acquisition offset: {0}", acquisition_offset);
            Console.WriteLine("MSA factor: {0}", msa_factor);
            Console.WriteLine("Firmware version: {0}", firmware.ToString());
            Console.WriteLine("Scan engine version: {0}", scan_engine.ToString());
            Console.WriteLine("LUT length: {0}", lut_data.Count);

            if (extended_lut)
            {
                Console.WriteLine("Acquisition channel: {0}", acquisition_channel);
                Console.WriteLine("Skip count: {0}", skip_count);
                Console.WriteLine("Min wavelength: {0}", min_wavelength);
                Console.WriteLine("Max wavelength: {0}", max_wavelength);
                Console.WriteLine("Increment: {0}", increment);
                Console.WriteLine("Extension range: {0}", extension_range);
                Console.WriteLine("Dark intensity endpoint: {0}", dark_intensity_endpoint);
            }

            if (lut_data.Count == 0 || extended_lut == false && lut_offset == 0)
                throw new InvalidDataException(Catalog.GetString("Cannot read LUT file: Invalid file format."));

            if (extended_lut)
            {
                return new Spectrometer.LUT(lut_data.ToArray(), msa_factor, timestamp, firmware, scan_engine, acquisition_channel,
                                            skip_count, min_wavelength, max_wavelength, increment, extension_range, dark_intensity_endpoint);
            }
            else
            {
                return new Spectrometer.LUT(lut_data.ToArray(), lut_offset, acquisition_offset, msa_factor, timestamp, firmware, scan_engine);
            }
        }
        
        private static object ParseLine(string line, string regexp, CultureInfo culture)
        {
            Regex parser;
            string[] split;
            
            parser = new Regex(regexp + "=");
            split  = parser.Split(line);
            
            if (split.Length > 1)
            {
                switch (regexp)
                {
                    
                case LUT.Parser.Culture:
                    return new CultureInfo(split[1]);

                case LUT.Parser.Timestamp:
                    DateTime dt;
                    try
                    {
                        dt = DateTime.ParseExact(split[1],
                                                 LUT.Parser.DateTimeFormat, 
                                                 DateTimeFormatInfo.CurrentInfo);
                    }
                        catch (Exception ex)
                    {
                        throw new FormatException(Catalog.GetString("Cannot parse header line: Invalid time format:") + 
                                                  " " + split[1], ex);
                    }
                    return dt;
                    
                case LUT.Parser.LutOffset:
                    return uint.Parse(split[1]);
                    
                case LUT.Parser.AcquisitionOffset:
                    return int.Parse(split[1]);

                case LUT.Parser.FirmwareVersion:
                case LUT.Parser.ScanEngineVersion:
                    return new Version(split[1]);

                case LUT.Parser.MsaFactor:
                    return double.Parse(split[1], culture);

                case LUT.Parser.AcquisitionChannelMask:
                    return byte.Parse(split[1], culture);

                case LUT.Parser.SkipCount:
                case LUT.Parser.DarkIntensityEndpoint:
                    return uint.Parse(split[1], culture);

                case LUT.Parser.MinWavelength:
                case LUT.Parser.MaxWavelength:
                case LUT.Parser.Increment:
                case LUT.Parser.ExtensionRange:
                    return float.Parse(split[1], culture);

                default:
                    throw new ArgumentException(Catalog.GetString("Cannot parse header line: Unknown parameter field.")); 
                }
            }

            throw new InvalidDataException();
        }

        public static void WriteHFormat(Spectrometer.LUT lut, Uri uri)
        {
            WriteHFormat(lut, Uri.UnescapeDataString(uri.AbsolutePath));
        }
        
        public static void WriteHFormat(Spectrometer.LUT lut, string fname)
        {
            if (Path.GetExtension(fname).ToLower() != ".h")
                fname = Path.ChangeExtension(fname, ".h");
                                               
            using (var stream = new FileIO.FileStreamWriter(fname))
            {
                stream.WriteLine("/* LUT origin: Hiperscan Class Library (HCL) */");
                stream.WriteLine("#ifndef LUT_H");
                stream.WriteLine("#define LUT_H");
                stream.WriteLine();
                stream.WriteLine("#define LUT_VERSION \"{0}\"", lut.Timestamp.ToString(LUT.CultureInfo));
                stream.WriteLine("const long lut_offset = {0};", lut.Offset);
                stream.WriteLine("const float lut[{0}] = {{", lut.Data.Length);
                
                CultureInfo culture = new CultureInfo("en-US");
                for (int ix=0; ix < lut.Data.Length; ++ix)
                {
                    if (ix < lut.Data.Length-1)
                        stream.WriteLine("  {0},", lut.Data[ix].ToString(culture));
                    else
                        stream.WriteLine("  {0} }};", lut.Data[ix].ToString(culture));
                }
                
                stream.WriteLine();
                stream.WriteLine("#endif");
            }
        }
        
        public static Spectrometer.LUT ReadHFormat(Uri uri)
        {
            return LUT.ReadHFormat(Uri.UnescapeDataString(uri.AbsolutePath));
        }

        public static Spectrometer.LUT ReadHFormat(string fname)
        {
            List<float> lut_data = new List<float>();
            
            uint lut_offset = 0;
            DateTime date_time = DateTime.MinValue;
            
            using (StreamReader stream = FileIO.OpenTextFile(fname))
            {
                string line;
                int skipped = 0;
                int line_number = 0;
                float max = float.MinValue;
                
                while ((line = stream.ReadLine()) != null)
                {
                    ++line_number;
                    
                    string s = line.Trim();
                    if (s.EndsWith(",", StringComparison.InvariantCulture))
                        s = s.Remove(s.Length-1);
                    if (s.EndsWith("};", StringComparison.InvariantCulture))
                        s = s.Remove(s.Length-2).Trim();
                    try
                    {
                        float f = float.Parse(s,
                                              NumberStyles.AllowDecimalPoint|
                                              NumberStyles.AllowExponent,
                                              LUT.CultureInfo);
                        if (f <= max)
                            throw new InvalidDataException(Catalog.GetString("A LUT must increase monotonically."));
                        
                        if (float.IsNaN(f))
                            throw new InvalidDataException(Catalog.GetString("A LUT must not contain NaNs."));
                        
                        max = f;
                        lut_data.Add(f);
                    }
                    catch (InvalidDataException)
                    {
                        throw;
                    }
                    catch (Exception)
                    {
                        ++skipped;
                        
                        try
                        {
                            lut_offset = (uint)LUT.ParseLineHFormat(line, LUT.ParserHFormat.LutOffset, LUT.CultureInfo);
                            continue;
                        } 
                        catch (InvalidDataException) {}
                        
                        try
                        {
                            date_time = (DateTime)LUT.ParseLineHFormat(line, LUT.ParserHFormat.DateTime, LUT.CultureInfo);
                            continue;
                        } 
                        catch (InvalidDataException) {}
                    }
                }
            }
            
            Console.WriteLine("LUT offset: {0}", lut_offset);
            Console.WriteLine("Date time: {0}",  date_time.ToString());
            Console.WriteLine("LUT length: {0}", lut_data.Count);
            
            if (lut_data.Count == 0 || lut_offset == 0)
                throw new InvalidDataException(Catalog.GetString("Cannot read LUT file: Invalid file format.")); 
            
            return new Spectrometer.LUT(lut_data.ToArray(), lut_offset, 0, double.NaN, date_time, new Version(0), new Version(0));
        }

        private static object ParseLineHFormat(string line, string regexp, CultureInfo culture)
        {
            Regex parser;
            string[] split;

            parser = new Regex(regexp);
            split = parser.Split(line);

            if (split.Length > 1)
            {
                switch (regexp)
                {

                    case LUT.ParserHFormat.LutOffset:
                        string s = split[1].Trim();
                        if (s.EndsWith(";", StringComparison.InvariantCulture))
                            s = s.Remove(s.Length - 1, 1);
                        return uint.Parse(s);

                    case LUT.ParserHFormat.DateTime:
                        s = split[1].Replace('"', ' ').Trim();
                        try
                        {
                            if (s.Contains("/"))
                                return DateTime.Parse(s, new CultureInfo("en-US"));
                            else
                                return DateTime.Parse(s, new CultureInfo("de-DE"));
                        }
                        catch
                        {
                            return DateTime.Parse(s, new CultureInfo("en-US"));
                        }

                    default:
                        throw new ArgumentException(Catalog.GetString("Cannot parse header line: Unknown regexp."));
                }
            }
            throw new InvalidDataException();
        }
    }
}
