/*******************************************************************************
* Copyright (C) 2014 - 2017 Intel Corp. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*  - Redistributions of source code must retain the above copyright notice,
*    this list of conditions and the following disclaimer.
*
*  - Redistributions in binary form must reproduce the above copyright notice,
*    this list of conditions and the following disclaimer in the documentation
*    and/or other materials provided with the distribution.
*
*  - Neither the name of Intel Corp. nor the names of its
*    contributors may be used to endorse or promote products derived from this
*    software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL Intel Corp. OR THE CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

using System;
using System.Linq;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Collections.Generic;

namespace FwUpdateApiSample
{
    /// <summary>
    /// This class includes various useful utility functions, some for internal use and some for
    /// applications to use
    /// </summary>
    public static class Utilities
    {
        /// <summary>
        /// Reads the NVM version from FW image file
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>NVM version as found in the file</returns>
        /// <remarks>
        /// For printing the version, refer to GetCurrentNvmVersionString() below or
        /// Utilities.NvmVersionToString()
        /// </remarks>
        public static UInt32 GetImageNvmVersion(string path)
        {
            var nvmVer = _RetreiveNvmVersion(path);
            return BitConverter.ToUInt32(nvmVer, 0);
        }

        /// <summary>
        /// Reads the full NVM version (major.minor) from FW image file
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>NVM version as found in the file</returns>
        public static string GetImageFullNvmVersion(string path)
        {
            var nvmVer = _RetreiveNvmVersion(path);
            var minor = nvmVer[0];
            var major = nvmVer[1];
            return $"{major:X2}.{minor:X2}";
        }

        private static byte[] _RetreiveNvmVersion(string path)
        {
            // TODO this method implementation currently works for Intel TBT products.
            const uint nvmMinorVersionOffset = 0x9;

            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                // Retrieve nvm version from file in case the current path is a container
                if (DeviceImageContainer.IsImageContainer(path))
                {
                    return DeviceImageContainer.GetImageFullNvmVersionFromContainer(fs);
                }
                var reader = new BinaryReader(fs);
                var headerLength = reader.ReadInt32() & RelativeNvmOffsetRetrieveMask;
                fs.Seek(headerLength + nvmMinorVersionOffset, SeekOrigin.Begin);
                var nvmVer = new byte[2];
                fs.Read(nvmVer, 0, 2);
                return nvmVer;
            }
        }

        /// <summary>
        /// Converts NVM version to string representation, assuming the version is valid
        /// </summary>
        /// <param name="version">NVM version to convert to string</param>
        /// <remarks>
        /// NVM number should be parsed like it's a hex number, but displayed like a decimal
        /// number, e.g. for rev 10 you get the value 16 (0x10) from the FW.
        /// </remarks>
        /// <returns>Strings representation of the NVM version</returns>
        public static string NvmVersionToString(UInt32 version)
        {
            return version.ToString("X");
        }

        /// <summary>
        /// The string used when version isn't available
        /// </summary>
        public const string NA = "N/A";

        /// <summary>
        /// The field is used when wanting to read relative nvm offset from image file
        /// </summary>
        public const uint RelativeNvmOffsetRetrieveMask = (1 << 24) - 1;

        /// <summary>
        /// Allows calling get current version functions in a safe manner which
        /// returns "N/A" when in safe mode
        /// </summary>
        /// <param name="func">Function object that reads current version and returns it as a string</param>
        /// <returns>Version string or "N/A" if the controller is safe-mode</returns>
        /// <exception>
        /// Rethrows any error thrown from func that isn't about safe-mode
        /// </exception>
        public static string SafeGetVersion(Func<string> func)
        {
            try
            {
                return func();
            }
            catch (TbtException e)
            {
                switch (e.ErrorCode)
                {
                    case TbtStatus.INVALID_OPERATION_IN_SAFE_MODE:
                    case TbtStatus.SDK_INVALID_OPERATION_IN_SAFE_MODE:
                        return NA;
                    default:
                        throw;
                }
            }
        }

        /// <summary>
        /// Reads the PD version from FW image file
        /// GetImagePdVersion is deprecated
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>PD version as found in the file</returns>
        [ObsoleteAttribute("This method is obsolete, for TI use GetImageTIPdVersion", true)]
        public static string GetImagePdVersion(string path)
        {
            return GetImageTIPdVersion(path);
        }

        /// <summary>
        /// Reads the PD version from FW image file
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>PD version as found in the file</returns>
        /// <remarks>
        /// Function tries to get the PD version by PD FW header. It looks for 'ACE1' identifier
        /// which should be located at offset 0x30. If that identifier is found, then the FW version
        /// will be at offset 0x34, otherwise it would try looking for the "TPS6598. HW.{5}FW" string.
        /// returns "N/A" if no PD version is found.
        /// This example works only with Texas Instrument (TI) chips TPS65982/3. 
        /// This should be implemented according to each
        /// specific PD solution.
        /// </remarks>
        public static string GetImageTIPdVersion(string path)
        {
            byte[] imageBuffer;

            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                var reader = new BinaryReader(fs);
                imageBuffer = reader.ReadBytes((int)fs.Length);
            }

            var sections = new DeviceFwInfo(new FileFwInfoSource(imageBuffer)).GetSectionInfo();
            if (!sections.ContainsKey(Sections.Pd))
            {
                return NA;
            }

            string version;

            var identifier =
                Encoding.ASCII.GetString(
                    imageBuffer.Skip((int)(sections[Sections.Pd].Offset + 0x30)).Take(4).ToArray());
            if (identifier == "ACE1")
            {
                var buffer =
                    BitConverter.ToString(
                        imageBuffer.Skip((int)(sections[Sections.Pd].Offset + 0x34)).Take(3).ToArray(), 0)
                        .Split('-');

                version = string.Format("{0}.{1}.{2}", buffer[1], buffer[0], buffer[2]);
            }
            else
            {
                const int versionFromFileLength = 10;
                var imageString = Encoding.ASCII.GetString(imageBuffer);

                var match = Regex.Match(imageString, "TPS6598. HW.{5}FW", RegexOptions.Singleline);
                if (!match.Success)
                {
                    return NA;
                }

                version = imageString.Substring(match.Index + match.Length, versionFromFileLength);
            }

            // Removing leading 0's in major version so that instead of 
            // (for example) version being 0001.01.00, version will be 1.01.00
            // (if major version is 0, version will be (for example) 0.08.12).
            // (Side note: the reason we don't use the "Version" class here
            // for the parsing is because we want to preserve the leading
            // zeros for the components besides the major version.)
            version = version.TrimStart('0');
            if (version[0] == '.')
            {
                version = "0" + version;
            }

            return version;
        }

        /// <summary>
        /// This method demonstrate how to use the I2CRead method for a 
        /// specific TI  PD controller It would work with PD controller
        /// TPS65982 and TPS65983 (but not limited to) 
        /// </summary>
        /// <param name="controller">controller ID</param>
        /// <returns>TI pd version as returned from the driver</returns>
        public static string GetTIPdInfo(SdkTbtBase controller)
        {
            // On port 1, offset and length of TI Pd version
            var data = BitConverter.ToInt32(controller.I2CRead(1, 0xF, 4), 0);
            var version = data.ToString("X");
            version = version.Insert(version.Length - 2, ".");
            version = version.Insert(version.Length - 5, ".");
            return version;
        }

        public class DeviceInformation
        {
            public UInt16 VendorId { get; set; }
            public UInt16 ModelId { get; set; }
        }

        /// <summary>
        /// Reads device information (vendor and model IDs) from FW image file
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>Device information (vendor and model IDs) as found in the file</returns>
        public static DeviceInformation GetImageDeviceInformation(string path)
        {
            var currentDeviceInformation = new DeviceInformation();
            var deviceInfo = _RetreiveDeviceImageInformation(path);

            // Set updated values in case of successful read
            if (deviceInfo.Any())
            {
                currentDeviceInformation.VendorId = BitConverter.ToUInt16(deviceInfo.ElementAt(0), 0);
                currentDeviceInformation.ModelId = BitConverter.ToUInt16(deviceInfo.ElementAt(1), 0);
            }

            return currentDeviceInformation;
        }

        private static IList<byte[]> _RetreiveDeviceImageInformation(string path)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                if (DeviceImageContainer.IsImageContainer(path))
                {
                    return DeviceImageContainer.GetImageDeviceInformationFromContainer(fs);
                }

                var reader = new BinaryReader(fs);
                var imageBuffer = reader.ReadBytes((int)fs.Length);
                var fileFwInfo = new DeviceFwInfo(new FileFwInfoSource(imageBuffer));
                var fileSections = fileFwInfo.GetSectionInfo();
                var fileHasDROM = fileSections.ContainsKey(Sections.DROM);
                if (!fileHasDROM)
                {
                    throw new TbtException(TbtStatus.SDK_NO_DROM_IN_FILE_ERROR);
                }

                var vendorLocation = new CheckLocation(offset: 0x10, length: 2, section: Sections.DROM);
                var modelLocation = new CheckLocation(offset: 0x12, length: 2, section: Sections.DROM);

                Func<CheckLocation, byte[]> extracter =
                    loc =>
                        imageBuffer.Skip((int)(fileSections[loc.Section].Offset + loc.Offset))
                            .Take((int)loc.Length)
                            .ToArray();

                return new List<byte[]>()
                {
                    extracter(vendorLocation),
                    extracter(modelLocation)
                };
            }
        }

        /// <summary>
        /// Reads from FW image file if the file is for host or device controller
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>Returns true when the image file is for host controller; otherwise - returns false</returns>
        public static bool GetImageIsHost(string path)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                var reader = new BinaryReader(fs);
                var imageBuffer = reader.ReadBytes((int)fs.Length);
                // Using DeviceFwInfo instead of HostFwInfo for getting only
                // the basic sections and not failing on non-existing ones
                var fileFwInfo = new DeviceFwInfo(new FileFwInfoSource(imageBuffer));
                var fileSections = fileFwInfo.GetSectionInfo();
                var isHostLocation = ImageValidator.GetIsHostCheckLocation();
                var val =
                    imageBuffer.Skip((int)(fileSections[isHostLocation.Section].Offset + isHostLocation.Offset))
                        .Take((int)isHostLocation.Length)
                        .ToArray();

                return System.Convert.ToBoolean(val[0] & isHostLocation.Mask);
            }
        }

        /// <summary>
        /// Reads "Native express" status from FW image file
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>"Native express" status as found in the file</returns>
        public static bool GetImageOsNativePciEnumerationStatus(string path)
        {
            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                var reader = new BinaryReader(fs);
                var imageBuffer = reader.ReadBytes((int)fs.Length);

                var fileFwInfo = new HostFwInfo(new FileFwInfoSource(imageBuffer));
                return fileFwInfo.OsNativePciEnumeration;
            }
        }

        /// <summary>
        /// This method is deprecated as only some old (now unsupported) host
        /// controllers didn't support EP update.
        ///
        /// Checks if a host controller supports device FW update
        /// </summary>
        /// <param name="controllerId">Controller ID as comes from the driver</param>
        /// <returns>True if the controller supports device FW update; otherwise - false</returns>
        [Obsolete("All of the currently supported host controllers support EP update", true)]
        public static bool SupportsDeviceUpdate(string controllerId)
        {
            return true;
        }

        /// <summary>
        /// Checks if a host controller is supported for FW update
        /// </summary>
        /// <param name="controllerId">Controller ID as comes from the driver</param>
        /// <returns>True if the controller is supported for FW update; otherwise - false</returns>
        public static bool HostUpdateSupported(string controllerId)
        {
            var id = _GetDevIdFromControllerId(controllerId);
            switch (id)
            {
                // Alplineridge
                case 0x1575:
                case 0x1576:
                case 0x1577:
                case 0x1578:
                case 0x15DD:
                case 0x15BF:
                case 0x15C0:
                case 0x15DC:
                case 0x15D9:
                case 0x15DA:
                case 0x15D2:
                case 0x15D3:
                case 0x15DE:
                case 0x15b5:
                case 0x15C1:
                case 0x15D4:
                case 0x15DB:
                // Titanridge
                case 0x15EC:
                case 0x15E9:
                case 0x15E7:
                case 0x15E8:
                case 0x15EA:
                case 0x15EB:
                case 0x15EF:
                    return true;
                default:
                    return false;
            }
        }

        /// <summary>
        /// This method is used to make sure this SDK supports the the firmware update through the given host controller.
        /// </summary>
        /// <param name="controllerId">Controller ID as comes from the driver</param>
        /// <returns>true if the controller is supported by the SDK; otherwise - false</returns>
        public static bool IsSupported(string controllerId)
        {
            try
            {
                GetHwConfiguration(controllerId);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Get HW configuration information from controller ID; helper function
        /// </summary>
        /// <param name="controllerId">Controller ID as comes from the driver</param>
        /// <remarks>
        /// Controller ID format is something like:
        /// <![CDATA[
        /// PCI\VEN_8086&DEV_156A&SUBSYS_00000000&REV_00\6&1e803622&0&000000E4_0
        /// ]]>
        /// The 4 hex-digits after "DEV_" are the device ID
        /// </remarks>
        /// <returns>HWInfo object with HW configuration</returns>
        private static HwInfo GetHwConfiguration(string controllerId)
        {
            return FwInfoSource.HwConfiguration(_GetDevIdFromControllerId(controllerId));
        }

        private static ushort _GetDevIdFromControllerId(string controllerId)
        {
            const string idStartString = "DEV_";
            const int idLength = 4;

            var idLocation = controllerId.IndexOf(idStartString) + idStartString.Length;
            var idString = controllerId.Substring(idLocation, idLength);
            return UInt16.Parse(idString, System.Globalization.NumberStyles.AllowHexSpecifier);
        }

        /// <summary>
        /// Check if a pointer comes from FW is valid
        /// </summary>
        /// <param name="pointer">Pointer value as comes from FW</param>
        /// <param name="pointerSize">
        /// The real size of the pointer (to be able to handle 24-bit pointer)
        /// </param>
        /// <returns>True if the pointer is valid, otherwise - false</returns>
        /// <remarks>Invalid pointer is NULL pointer or -1 (0xFFFFFFFF in uint)</remarks>
        internal static bool ValidPointer(uint pointer, int pointerSize)
        {
            var invalidPointer = BitConverter.GetBytes(pointer).Take(pointerSize).All(b => b == byte.MaxValue);
            return pointer != 0 && !invalidPointer;
        }

        internal static void WrapWithConvertToTbtException(Action action)
        {
            try
            {
                action();
            }
            catch (Exception e)
            {
                Logger.Instance.LogErr($"Got exception: {e}");
                if (e is FwUpdateDriverApi.FuDrvApiException)
                {
                    e = new TbtException(((FwUpdateDriverApi.FuDrvApiException)e).Code);
                }
                throw e;
            }
        }

        public static bool IsIntelDevice(ushort deviceId)
        {
            switch (deviceId)
            {
                // AR
                case 0x1576:
                case 0x1578:
                case 0x15C0:
                case 0x15DA:
                case 0x15D3:
                case 0x15b5:
                case 0x15C1:
                case 0x15D4:
                case 0x15DB:
                // TR
                case 0x15EC:
                case 0x15E9:
                case 0x15E7:
                case 0x15EA:
                case 0x15EF:
                // GR
                case 0x0B26:
                case 0x0B40:
                // DBR
                case 0x15F1:
                    return true;
                default:
                    return false;
            }
        }

        public static bool IsLegacyHost(ushort devid)
        {
            switch (devid)
            {
                case 0x1575 :
                case 0x1577 :
                case 0x15BF :
                case 0x15D2 :
                case 0x15D9 :
                case 0x15DC :
                case 0x15DD :
                case 0x15DE :
                case 0x15E8 :
                case 0x15EB :
                    return true;
                default:
                    return false;
            }
        }

        public static bool IsLegacyDevice(ushort devid)
        {
            switch (devid)
            {
                case 0x1576:
                case 0x1578:
                case 0x15C0:
                case 0x15DA:
                case 0x15D3:
                case 0x15b5:
                case 0x15C1:
                case 0x15D4:
                case 0x15DB:
                case 0x15EC:
                case 0x15E9:
                case 0x15E7:
                case 0x15EA:
                case 0x15EF:
                    return true;
                default:
                    return false;
            }
        }

        /// <summary>
        /// Reads device id from FW image file
        /// </summary>
        /// <param name="path">Path to FW image file to read from</param>
        /// <returns>ushort representing device Id</returns>
        public static ushort RetrieveDeviceIdFromImage(string path)
        {
            const uint deviceIdStartOffset = 0x5; // Offset of device id in image

            using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                // Retrieve device id from container
                if (DeviceImageContainer.IsImageContainer(path))
                {
                    return DeviceImageContainer.GetDeviceIdFromContainer(fs);
                }

                var reader = new BinaryReader(fs);
                // Read first 3 bytes which indicate the start of the relative section in NVM
                var relativeOffset = reader.ReadInt32() & RelativeNvmOffsetRetrieveMask;
                fs.Seek(relativeOffset, SeekOrigin.Begin);  // Set position to relative offset
                fs.Seek(deviceIdStartOffset, SeekOrigin.Current);   // Advance pointer to device id location in image

                // Read bytes located at address 5-6 (zero index) which indicate the device id
                return BitConverter.ToUInt16(reader.ReadBytes(2), 0);
            }
        }

        public static string GetDeviceType(ushort deviceId)
        {
            switch (deviceId)
            {
                // AR
                case 0x1576:
                case 0x1578:
                case 0x15C0:
                case 0x15DA:
                case 0x15D3:
                case 0x15b5:
                case 0x15C1:
                case 0x15D4:
                case 0x15DB:
                    return "AR";
                // TR
                case 0x15EC:
                case 0x15E9:
                case 0x15E7:
                case 0x15EA:
                case 0x15EF:
                    return "TR";
                // GR
                case 0x0B26:
                case 0x0B40:
                    return "GR";
                // DBR
                case 0x15F1:
                    return "DBR";
                default:
                    return NA;
            }
        }

    }
}