using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Blocks.Step7V5;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders;
using DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders.Step7V5;
using DotNetSiemensPLCToolBoxLibrary.Projectfiles;
using Security;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class app_custom_screens_IHConfiguration_data_sources_configuration_v2_api_import_plc_project : System.Web.UI.Page
{
    private class DBOptions
    {
        public string name { get; set; }
        public bool overrideAlias { get; set; }
    }

    private class PLCImportedDB
    {
        public string Name { get; set; }
        public List<PLCImportedTag> Tags { get; set; }
        public bool HasConflicts { get; set; }
        public List<String> Conflicts { get; set; }

        public PLCImportedDB()
        {
            Tags = new List<PLCImportedTag>();
            Conflicts = new List<String>();
            HasConflicts = false; 
        }
    }

    private class PLCImportedTag
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Alias { get; set; }
        public bool HasConflicts { get; set; }

        public PLCImportedTag()
        {
            HasConflicts = false; 
        }
    }

    private class ImportPLCProjectParams
    {
        public List<DBOptions> dbs { get; set; }
        public int accountNumber { get; set; }
        public string filename { get; set; }
        public string folderName { get; set; }
        public int agentId { get; set; }
    }

    private class DBTags
    {
        public String Name;
        public List<DBTag> Tags = new List<DBTag>(); 
    }

    private class DBTag
    {
        public int Id;
        public String Name;
        public String Alias;
        public String DataType;
    }

    public class DataBlock
    {
        public string Name { get; set; }
        public int TagsCount { get; set; }
        public List<S7DataRow> S7DataRows { get; set; }
    }

    public class S7DataRowProperties
    {
        public S7DataRowType Type;
        public int ByteAddress;
        public int BitAddress; 
    }

    //public class S7DataRowExtended : S7DataRow
    //{
    //    public bool MatchFound = false;
        
    //    public S7DataRow(S7DataRow dr) : this(dr.Name, dr.DataType, dr.PlcBlock)
    //    {
    //        this.ArrayStart = dr.ArrayStart;
    //        this.ArrayStop = dr.ArrayStop;
    //        this.Attributes = dr.Attributes;
    //        this.BlockAddress = dr.BlockAddress;
    //        //this.BlockAddressInDbFormat = dr.BlockAddressInDbFormat; 
    //        //this.ByteLength = dr.ByteLength;
    //        this.Children = dr.Children;
    //        this.Comment = dr.Comment;
    //        this.CurrentBlock = dr.CurrentBlock;
    //        this.DataType = dr.DataType;
    //        //this.DataTypeAsString = dr.DataTypeAsString;
    //        this.DataTypeBlockNumber = dr.DataTypeBlockNumber;
    //        //this.Dependencies = dr.Dependencies;
    //        //this.FullComment = dr.FullComment;
    //        //this.FullName = dr.FullName;
    //        this.IsArray = dr.IsArray;
    //        this.isInOut = dr.isInOut;
    //        this.isRootBlock = dr.isRootBlock;
    //        this.Name = dr.Name;
    //        //this.NextBlockAddress = dr.NextBlockAddress;
    //        this.OrginalChildren = dr.OrginalChildren;
    //        this.Parent = dr.Parent;
    //        //this.PlcBlock = dr.PlcBlock;
    //        //this.PlcTag = dr.PlcTag;
    //        this.ReadOnly = dr.ReadOnly;
    //        this.StartValue = dr.StartValue;
    //        //this.StartValueAsString = dr.StartValueAsString;
    //        this.StringSize = dr.StringSize;
    //        //this.StructuredName = dr.StructuredName;
    //        //this.TimeStampConflict = dr.TimeStampConflict;
    //        this.Value = dr.Value;
    //        //this.ValueAsString = dr.ValueAsString;            

    //    }
        
    //    public S7DataRow(string name, S7DataRowType datatype, Block plcblock)
    //        : base(name, datatype, plcblock)
    //    {
    //    }
    //}

    private String connectionString = ConfigurationManager.ConnectionStrings["SYSTEM"].ConnectionString;

    protected void Page_Load(object sender, EventArgs e)
    {
        System.Web.Script.Serialization.JavaScriptSerializer jss = new System.Web.Script.Serialization.JavaScriptSerializer();
        System.IO.StreamReader sr = new System.IO.StreamReader(Page.Request.InputStream);
        String str = System.Web.HttpUtility.UrlDecode(sr.ReadToEnd());
        ImportPLCProjectParams importPLCProjectParams = jss.Deserialize<ImportPLCProjectParams>(str);

        string internalPath = "\\app\\custom-screens\\IHConfiguration\\data-sources-configuration-v2\\plc_projects_uploaded\\"; 
        string path = AppDomain.CurrentDomain.BaseDirectory + internalPath;
        string filepath = path + importPLCProjectParams.filename; 

        
        List<DBTags> dbTags = GetAgentTags(importPLCProjectParams);
        List<DataBlock> datablocks = GetPLCProjectDataBlocks(filepath, importPLCProjectParams);

        //filter datablocks to remove data that is not yet supported.
        foreach (DataBlock db in datablocks)
        {
            db.S7DataRows.AddRange(GetStructDataRowsList(db.S7DataRows.Where(dr => dr.DataType == S7DataRowType.STRUCT).ToList())); 
	        db.S7DataRows.AddRange(GetStructDataRowsList(db.S7DataRows.Where(dr => dr.DataType == S7DataRowType.UDT).ToList())); 
            
            db.S7DataRows.RemoveAll(dr => !(new[] { S7DataRowType.BOOL,S7DataRowType.BYTE,S7DataRowType.DINT, 
                                                    S7DataRowType.DWORD,S7DataRowType.INT,S7DataRowType.REAL,
                                                    S7DataRowType.WORD }
                                            ).Contains(dr.DataType));
        }

        List<PLCImportedDB> importedDBs = new List<PLCImportedDB>(); 

        //comparing and importing.
        List<DBTag> tagsWithoutMatch = new List<DBTag>();
        List<S7DataRow> matchedDataRows = new List<S7DataRow>(); 
        foreach (DBOptions dbOption in importPLCProjectParams.dbs)
        {
            PLCImportedDB impDB = new PLCImportedDB()
            {
                Name = dbOption.name
            };
            importedDBs.Add(impDB); 

            DataBlock projectDB = datablocks.SingleOrDefault(db => db.Name.Trim().ToUpper().Equals(dbOption.name.Trim().ToUpper()));
            DBTags plcTags = dbTags.SingleOrDefault(dbt => dbt.Name.Trim().ToUpper().Equals(dbOption.name.Trim().ToUpper()));
            if (projectDB != null && plcTags != null)
            {

                int projectTagsCount = projectDB.S7DataRows.Count
                                        + projectDB.S7DataRows.Where(c => c.DataType == S7DataRowType.STRUCT).Sum(p => p.Children.Count)
                                        - projectDB.S7DataRows.Where(c => c.DataType == S7DataRowType.STRUCT).ToList().Count
                                        + projectDB.S7DataRows.Where(c => ((S7DataRow)c).IsArray).Sum(p => ((S7DataRow)p).ArrayStop[0] - ((S7DataRow)p).ArrayStart[0] + 1)
                                        - projectDB.S7DataRows.Where(c => ((S7DataRow)c).IsArray).ToList().Count; 

                foreach (DBTag tag in plcTags.Tags)
                {
                    bool skip = false;

                    S7DataRowProperties tagProps = GetS7DataRowPropertiesFromTagDBName(tag.Name);
                    if (tagProps.Type == S7DataRowType.STRING || tagProps.Type == S7DataRowType.CHAR)
                    {
                        skip = true;
                    }

                    int arrayStart = -1;
                    int arrayStop = -1;
                    int arrayIndex = -1;
                    bool isArray = (tag.Name.IndexOf('[') != -1 && tag.Name.IndexOf(']') != -1);
                    if (isArray)
                    {
                        string arrayStr = tag.Name.Substring(tag.Name.IndexOf('['), tag.Name.IndexOf(']') - tag.Name.IndexOf('[') + 1);
                        string[] arrayStrSplitted = arrayStr.Replace("[", "").Replace("]", "").Split(new string[] { ".." }, StringSplitOptions.None);
                        arrayStart = Convert.ToInt32(arrayStrSplitted[0]);
                        arrayStop = Convert.ToInt32(arrayStrSplitted[1]);
                        arrayIndex = Convert.ToInt32(tag.Name.Split('[')[2].Replace("]", ""));
                    }

                    string blockAddressInDBFormat = ((tag.Name.Substring(tag.Name.IndexOf('.') + 1)).Split('[')[0]).Trim();

                    if (!skip)
                    {
                        S7DataRow match = FindMatchDataRow(projectDB.S7DataRows, tag);
                        bool hasConflicts = false;

                        if (match == null)
                        {
                            impDB.HasConflicts = hasConflicts = true;
                            if (!impDB.Conflicts.Exists(p => p == "TAG_NOT_FOUND")) impDB.Conflicts.Add("TAG_NOT_FOUND");

                            tagsWithoutMatch.Add(tag);
                        }
                        else
                        {
                            matchedDataRows.Add(match); 
                        }

                        string alias = null;
                        if ((dbOption.overrideAlias == true || String.IsNullOrEmpty(tag.Alias)) && !hasConflicts)
                        {
                            alias = match.Name + ((isArray) ? "[" + arrayIndex.ToString() + "]" : "");
                        }
                        else
                        {
                            alias = tag.Alias;
                        }

                        PLCImportedTag impTag = new PLCImportedTag()
                        {
                            Id = tag.Id,
                            Name = tag.Name,
                            Alias = alias,
                            HasConflicts = hasConflicts,
                        };

                        impDB.Tags.Add(impTag);
                    }
                }

                if (projectTagsCount != plcTags.Tags.Count)
                {
                    impDB.HasConflicts = true;
                    if (!impDB.Conflicts.Exists(p => p == "PLC_PROJECT_TAGS_COUNT_MISMATCH")) impDB.Conflicts.Add("PLC_PROJECT_TAGS_COUNT_MISMATCH"); 
                }
            }
            else
            {
                impDB.HasConflicts = true;
                if (!impDB.Conflicts.Exists(p => p == "DB_NOT_FOUND")) impDB.Conflicts.Add("DB_NOT_FOUND"); 
            }
        }

        //creating error report
        string errorReportTXTPath = path + importPLCProjectParams.filename + "-error-report.txt";
        StreamWriter sw = new StreamWriter(errorReportTXTPath, false);
        sw.WriteLine("------------------------------------------------------");
        sw.WriteLine("Project Mnemonics that don't match with PLC tags:");
        sw.WriteLine("------------------------------------------------------");
        foreach (DataBlock db in datablocks)
        {
            List<S7DataRow> missingDRs = db.S7DataRows.Where(dr => !matchedDataRows.Contains(dr)).ToList();
            missingDRs.ForEach(m => sw.WriteLine(db.Name + " - " + m.BlockAddressInDbFormat + " - " + m.FullName)); 
        }
        sw.WriteLine("------------------------------------------------------");
        sw.WriteLine("PLC tags that don't match with project:");
        sw.WriteLine("------------------------------------------------------");
        tagsWithoutMatch.ForEach(tag => sw.WriteLine(tag.Name)); 
        sw.Close();

        string errorReportVirtualPath = Request.ApplicationPath + internalPath + importPLCProjectParams.filename + "-error-report.txt";

        string json = jss.Serialize(new { importedDBs = importedDBs, errorReportPath = errorReportVirtualPath });

        //json = "{\"status\":\"OK\"}";
        Response.Clear();
        Response.ContentType = "application/json; charset=utf-8";
        Response.Write(json);
        Response.End();

    }

    private List<S7DataRow> GetStructDataRowsList(List<S7DataRow> structRows)
    {
	    List<S7DataRow> datarows = structRows.SelectMany(d => d.Children).Select(d => (S7DataRow)d).ToList();
        List<S7DataRow> structs = datarows.Where(dr => dr.DataType == S7DataRowType.STRUCT).ToList();
        if (structs.Count > 0)
        {
            datarows.AddRange(GetStructDataRowsList(structs));
        }
        datarows.RemoveAll(dr => dr.DataType == S7DataRowType.STRUCT);

        List<S7DataRow> udts = datarows.Where(dr => dr.DataType == S7DataRowType.UDT).ToList();
        if (udts.Count > 0)
        {
            datarows.AddRange(GetStructDataRowsList(udts));
        }
        datarows.RemoveAll(dr => dr.DataType == S7DataRowType.UDT);

        return datarows;
    }

    private S7DataRow FindMatchDataRow(List<S7DataRow> dataRows, DBTag tag)
    {
        S7DataRow match = null; 
        S7DataRowProperties tagProps = GetS7DataRowPropertiesFromTagDBName(tag.Name);

        int arrayStart = -1;
        int arrayStop = -1; 
        bool isArray = (tag.Name.IndexOf('[') != -1 && tag.Name.IndexOf(']') != -1);
        if (isArray) {
            string arrayStr = tag.Name.Substring(tag.Name.IndexOf('['), tag.Name.IndexOf(']') - tag.Name.IndexOf('[') + 1);
            string[] arrayStrSplitted = arrayStr.Replace("[", "").Replace("]", "").Split(new string[] { ".." }, StringSplitOptions.None);
            arrayStart = Convert.ToInt32(arrayStrSplitted[0]);
            arrayStop = Convert.ToInt32(arrayStrSplitted[1]);
        }

        string blockAddressInDBFormat = ((tag.Name.Substring(tag.Name.IndexOf('.') + 1)).Split('[')[0]).Trim();

        foreach (S7DataRow dr in dataRows)
        {
            if (dr.DataType == S7DataRowType.STRUCT)
            {
                match = FindMatchDataRow(dr.Children.Select(d => (S7DataRow)d).ToList(), tag);
                if (match != null) break; 
            }

            if (((dr.IsArray == true && dr.IsArray == isArray && dr.ArrayStart[0] == arrayStart
                && dr.ArrayStop[0] == arrayStop) || (dr.IsArray == false && dr.IsArray == isArray))
                && dr.DataType == tagProps.Type && dr.BlockAddressInDbFormat == blockAddressInDBFormat)
            {
                match = dr;
                break; 
            }
        }
        
        return match; 
    }

    private S7DataRowProperties GetS7DataRowPropertiesFromTagDBName(string name)
    {
        S7DataRowProperties props = new S7DataRowProperties()
        {
            Type = S7DataRowType.ANY, 
        }; 
        
        try
        {
            string type = name.Split('.')[1].Substring(0, 3);
            string blockAddress = (name.Substring(name.IndexOf('.') + 1)).Replace(type, "").Split('[')[0]; 

            switch (type.Trim().ToUpper())
            {
                case "DBX":
                    props.Type = S7DataRowType.BOOL;
                    props.ByteAddress = Convert.ToInt32(blockAddress.Split('.')[0]);
                    props.BitAddress = Convert.ToInt32(blockAddress.Split('.')[1]); 
                    break;
                case "DBS":
                    props.Type = S7DataRowType.STRING;
                    //not implemented
                    break;
                case "DBC":
                    props.Type = S7DataRowType.CHAR;
                    //not implemented
                    break;
                case "DBI":
                    props.Type = S7DataRowType.INT;
                    props.ByteAddress = Convert.ToInt32(blockAddress); 
                    break;
                case "DBJ":
                    props.Type = S7DataRowType.DINT;
                    props.ByteAddress = Convert.ToInt32(blockAddress); 
                    break;
                case "DBU":
                    props.Type = S7DataRowType.DWORD;
                    props.ByteAddress = Convert.ToInt32(blockAddress); 
                    break;
                case "DBW":
                    props.Type = S7DataRowType.WORD;
                    props.ByteAddress = Convert.ToInt32(blockAddress); 
                    break;
                case "DBB":
                    props.Type = S7DataRowType.BYTE;
                    props.ByteAddress = Convert.ToInt32(blockAddress); 
                    break;
                case "DBD":
                    props.Type = S7DataRowType.REAL;
                    props.ByteAddress = Convert.ToInt32(blockAddress); 
                    break; 
                default:
                    props.Type = S7DataRowType.ANY;
                    break; 
            }
        }
        catch (Exception ex) {
        }

        return props; 
    }

    private List<DataBlock> GetPLCProjectDataBlocks(String filepath, ImportPLCProjectParams plcParams)
    {
        List<DataBlock> dataBlocks = new List<DataBlock>();
        try
        {
            Project tmp = Projects.LoadProject(filepath, false);

            ProjectFolder pf = null;
            int pfCount = 0;
            while (pf == null && pfCount < 5)
            {
                try
                {
                    pfCount++;
                    pf = tmp.ProjectStructure;
                }
                catch (Exception ex) { }
            }

            List<BlocksOfflineFolder> blocksOfflineFolders = new List<BlocksOfflineFolder>();
            BlocksOfflineFolder folder = null; 
            if (pf != null && pf.SubItems != null)
            {
                blocksOfflineFolders = GetPLCBlocksOfflineFolders(pf.SubItems);
                folder = blocksOfflineFolders.SingleOrDefault(bof => this.GetOfflineFolderName((ProjectFolder)bof).ToUpper() == plcParams.folderName.ToUpper());
            }

            List<ProjectBlockInfo> blocks = ((IBlocksFolder)folder).readPlcBlocksList();
            foreach (ProjectBlockInfo block in blocks)
            {
                try
                {
                    Block b = block.GetBlock();
                    if (b.BlockType == DotNetSiemensPLCToolBoxLibrary.DataTypes.PLCBlockType.DB
                        && plcParams.dbs.Exists(item => item.name.Equals(b.BlockName)))
                    {
                        dataBlocks.Add(
                            new DataBlock()
                            {
                                Name = b.BlockName,
                                TagsCount = ((S7DataBlock)b).Structure.Children.Count,
                                S7DataRows = ((S7DataBlock)b).Structure.Children.Select(item => (S7DataRow)item).ToList(),
                            }
                        );
                    }
                }
                catch (Exception ex) { }
            }
        }
        catch (Exception ex)
        {
            //log error?
        }
        return dataBlocks; 
    }

    private string GetOfflineFolderName(ProjectFolder bof)
    {
        if (bof.Parent != null && bof.Parent.Parent != null)
        {
            return GetOfflineFolderName(bof.Parent);
        }
        else
        {
            return bof.Name;
        }
    }

    private List<BlocksOfflineFolder> GetPLCBlocksOfflineFolders(List<DotNetSiemensPLCToolBoxLibrary.DataTypes.Projectfolders.ProjectFolder> subItems)
    {
        List<BlocksOfflineFolder> blocks = new List<BlocksOfflineFolder>();
        foreach (ProjectFolder item in subItems)
        {
            if (item.GetType() == typeof(BlocksOfflineFolder))
            {
                blocks.Add((BlocksOfflineFolder)item);
            }

            if (item.SubItems != null)
            {
                blocks.AddRange(GetPLCBlocksOfflineFolders(item.SubItems));
            }
        }
        return blocks;
    }

    private List<DBTags> GetAgentTags(ImportPLCProjectParams importPLCProjectParams)
    {
        List<DBTags> dbTags = new List<DBTags>();
        try
        {
            SqlConnection sqlCon = new SqlConnection();
            sqlCon.ConnectionString = connectionString;

            SqlCommand comm = new SqlCommand("SYSTEM.GetAccountTags", sqlCon);
            comm.CommandType = CommandType.StoredProcedure;

            SqlParameter sqlParam = new SqlParameter("@AccountNumber", SqlDbType.Int);
            sqlParam.Value = importPLCProjectParams.accountNumber;
            comm.Parameters.Add(sqlParam);

            SqlParameter sqlParam1 = new SqlParameter("@DBs", SqlDbType.VarChar);
            sqlParam1.Value = (String.Join(",", importPLCProjectParams.dbs.Select(x => x.name).ToArray())).Replace("DB", "");
            comm.Parameters.Add(sqlParam1);

            SqlParameter sqlParam2 = new SqlParameter("@AgentId", SqlDbType.Int);
            sqlParam2.Value = importPLCProjectParams.agentId;
            comm.Parameters.Add(sqlParam2);

            DataSet ds = DALSecurity.CallSecuredProcedure(sqlCon, comm);
            if (ds != null)
            {
                foreach (DataRow r in ds.Tables[0].Rows)
                {
                    DBTag tag = new DBTag() { Id = Convert.ToInt32(r["Id"]), Name = r["Name"].ToString(), Alias = r["Alias"].ToString(), DataType = r["DataType"].ToString() };
                    if (!dbTags.Exists(db => db.Name == tag.Name.Split('.')[0]))
                    {
                        dbTags.Add(new DBTags() { Name = tag.Name.Split('.')[0] });
                    }
                    DBTags tags = dbTags.Single(db => db.Name == tag.Name.Split('.')[0]);
                    tags.Tags.Add(tag);
                }
            }
        }
        catch (Exception ex)
        {
            //log error?
        }

        return dbTags;
    }
}