/// <summary>
        /// Examines the tags and merges duplicates together
        /// </summary>
        /// <remarks>
        /// This was added because Version#1 of the file would often have mixed results, such that there were two
        /// MyTag1 objects.  One of the objects would have the PV and SP parameters and the other would have
        /// a OP parameter.  This method will find those duplicates and "normalize" them so that there is only
        /// one instance containing all of the parameters.
        /// </remarks>
        private void Normalize()
        {
            Dictionary <string, HmiWebMetaData> normalizedTaglist = new Dictionary <string, HmiWebMetaData>();

            foreach (HmiWebMetaData tag in Tags)
            {
                HmiWebMetaData normalizedTag = null;
                string         key           = tag.TagName.ToLower();
                if (!normalizedTaglist.TryGetValue(key, out normalizedTag))
                {
                    normalizedTag = tag;
                    normalizedTaglist.Add(key, normalizedTag);
                }
                else
                {
                    //Add any parameters that need to be added.
                    foreach (string parameter in tag.Parameters)
                    {
                        if (!normalizedTag.Parameters.Contains(parameter))
                        {
                            normalizedTag.Parameters.Add(parameter);
                        }
                    }

                    if (string.IsNullOrWhiteSpace(normalizedTag.FaceplatePartialFileName))
                    {
                        normalizedTag.FaceplatePartialFileName = tag.FaceplatePartialFileName;
                    }
                }

                //It seems like the Deserialize can't handle the "Parameters=""" properly.  It thinks there
                //is a blank parameter.  Remove it.
                if (normalizedTag.Parameters.Contains(""))
                {
                    normalizedTag.Parameters.Remove("");
                }
            }

            Tags = normalizedTaglist.Values.ToList();
        }
        /// <summary>
        /// Read the passed in HMIWeb graphic file (by loading into an HTML document),
        /// and write out all the tags to a metadata ".ucsparsed" file for quick searching.
        /// The file will not be created if one already exists and is newer than the HMIWeb file
        /// </summary>
        /// <param name="fullHmiWebGraphicFileName">The full HMIWeb graphics file name</param>
        /// <param name="infoMessages">Informational messages created as part of the parsing</param>
        /// <param name="errorMessages">Error messages created as a result of failing to parse the file</param>
        /// <returns>
        /// The contents of the HMIWeb Graphics file as it has been parsed and normalized.
        /// </returns>
        static public HmiWebParseFile ParseHmiWebGraphic(string fullHmiWebGraphicFileName, out List <string> infoMessages, out List <string> errorMessages)
        {
            infoMessages  = new List <string>();
            errorMessages = new List <string>();

            //If the HMIWeb graphic doesn't exist, return immediately
            if (!File.Exists(fullHmiWebGraphicFileName))
            {
                errorMessages.Add(string.Format("HMIWeb file {0} could not be opened by the search feature.", fullHmiWebGraphicFileName));
                return(null);
            }

            HmiWebParseFile retVal           = null;
            bool            createFile       = true;                                      // Is true if the metadata file needs to be created
            string          metadataFileName = fullHmiWebGraphicFileName + FileExtension; // Name of the metadata file

            //If the metadata file exists, we want to check to see if we need to re-create it or not.
            //We will re-create it if the the metadata file is older than the HMIWeb file
            if (File.Exists(metadataFileName))
            {
                //File exists, we don't have to create it.
                createFile = File.GetLastWriteTime(fullHmiWebGraphicFileName) > File.GetLastWriteTime(metadataFileName);

                if (!createFile)
                {
                    try
                    {
                        //Great.  We can save time by just reading in the existing metadata file.
                        retVal = HmiWebParseFile.Deserialize(metadataFileName);

                        if (retVal != null && retVal.Version != HmiWebParseFile.Version3)
                        {
                            //Whoops - we need to upgrade the file.  It was created with an older
                            //version of the parser.
                            retVal = null; //Setting to null will trigger a CreateFile = true below.
                        }
                    }
                    catch (Exception ex)
                    {
                        //Failed to load the file.  We'll log the message and then indicate that we need
                        //to re-create the file since it is mostly likely in an unknown format.
                        infoMessages.Add(string.Format("Failed to load '{0}'.  The file will be re-created\n\n{1}", metadataFileName, ex.Message));
                        retVal = null;  //Setting to null will trigger a CreateFile = true below.
                    }
                    finally
                    {
                        //If there was some problem loading the search file, we'll re-create it.
                        if (retVal is null)
                        {
                            createFile = true;
                        }
                    }
                }
            }

            //If things have gone well, we have read in an existing metdata data file which will save us a lot of
            //time.  But if the metadata file didn't exist, or there was some other error, we'll have to take
            //the more time consuming route and parse the HMIWeb graphic file.
            if (createFile)
            {
                try
                {
                    //Read the entire HMIWeb file into memory
                    string fileContents = File.ReadAllText(fullHmiWebGraphicFileName);

                    //Put the text into an in-memory browser so that we can parse it.
                    using (WebBrowser browser = new WebBrowser())
                    {
                        browser.ScriptErrorsSuppressed = true;
                        browser.DocumentText           = fileContents;
                        browser.Document.OpenNew(true);
                        browser.Document.Write(fileContents);
                        browser.Refresh();

                        HtmlDocument pHtmlDoc3 = browser.Document;

                        //Create the HmiWebParseFile object that we will populate.
                        retVal = new HmiWebParseFile();

                        //We are looking for something like the following ...
                        //<DIV
                        //  tabIndex = -1 id = shape078 class=hsc.shape.1 linkType = "embedded" globalscripts = "" styleClass = "" numberOfShapesAnimated = "1" value = "1"
                        //  style="FONT-SIZE: 0px; TEXT-DECORATION: none; HEIGHT: 44px; FONT-FAMILY: Arial; WIDTH: 94px; POSITION: absolute; FONT-WEIGHT: 400; FONT-STYLE: normal; LEFT: 119px; TOP: 38px; BEHAVIOR: url(#HSCShapeLinkBehavior) url(#HSCShapeLinkBehavior#shapelinkanimator) url(#HDXVectorFactory#shapelink) url(#BindingBehavior); VISIBILITY: inherit"
                        //  hdxproperties="fillColorBlink:False;HDXBINDINGID:1;Height:44;lineColorBlink:False;Width:94;"
                        //  src = ".\Debutanizer_files\REGCTL_SIM2_U.sha"
                        //  parameters = "Text?Version:USO-R300;Point?tagname:11FC01;Text?FaceplateFile:Sim2\Faceplate\PID_REGCTL_SIM2_FP_U.htm;Parameter?cp_eudesc:EUDESC;Parameter?cp_mode:MODE;Parameter?cp_pntalm:PTINAL;Parameter?cp_pntalmack:ACKSTAT;Parameter?cp_pv:PV;Parameter?cp_pvformat:PVFORMAT;Parameter?cp_sp:SP;Parameter?cp_statetxt0:STATETXT0;Parameter?cp_statetxt1:STATETXT1;Parameter?cp_statetxt2:STATETXT2;Point?AssetName:11FC01@USD;"
                        // >
                        //Get all DIV tags from the HTML document
                        HtmlElementCollection pDivCollection = pHtmlDoc3.GetElementsByTagName("DIV");
                        foreach (HtmlElement pDivElement in pDivCollection)
                        {
                            // Now get the "parameters" attribute
                            string attributeValue = pDivElement.GetAttribute("parameters");

                            // Now find the "tagname" and "assetName" parameters and create XML element for each (if found)
                            // Will be of the format ?tagname:11HC01; or ?AssetName:11HC01;
                            string tagName = GetParameterValue(attributeValue, TagnameIdentifier);
                            if (!string.IsNullOrWhiteSpace(tagName))
                            {
                                HmiWebMetaData tagInfo = new HmiWebMetaData(tagName);

                                //We have a tagname.  If we have a faceplate, then we will potentially have a
                                //trendable point ... which means that we want to search for the trendable parameters
                                string faceplateFile = GetParameterValue(attributeValue, FaceplateFileIdentifier);
                                if (!string.IsNullOrWhiteSpace(faceplateFile))
                                {
                                    //Store off the "short" faceplate name associated with this tag
                                    tagInfo.FaceplatePartialFileName = faceplateFile;

                                    bool foundPv = false;
                                    bool foundSp = false;

                                    //Look for the PV, SP, and OP parameters
                                    string pv = GetParameterValue(attributeValue, PVIdentifier);
                                    if (!string.IsNullOrWhiteSpace(pv))
                                    {
                                        tagInfo.Parameters.Add(pv);
                                        foundPv = true;
                                    }

                                    string sp = GetParameterValue(attributeValue, SPIdentifier);
                                    if (!string.IsNullOrWhiteSpace(sp))
                                    {
                                        tagInfo.Parameters.Add(sp);
                                        foundSp = true;
                                    }

                                    string op = GetParameterValue(attributeValue, OPIdentifier);
                                    if (!string.IsNullOrWhiteSpace(op))
                                    {
                                        tagInfo.Parameters.Add(op);
                                    }
                                    else if (foundPv && foundSp)
                                    {
                                        //We are looking for trendable parameters.  In many cases, the item on the main graphic
                                        //will display both PV and SP to the user.  If the user clicks on the icon, it will
                                        //bring up a faceplate containing trending information for PV, SP, and OP.  So if we
                                        //find something with both PV and SP, we'll automatically add OP.
                                        tagInfo.Parameters.Add("OP");
                                    }
                                }

                                retVal.Tags.Add(tagInfo);
                            }

                            string assetName = GetParameterValue(attributeValue, AssetNameIdentifier);
                            if (!string.IsNullOrWhiteSpace(assetName))
                            {
                                retVal.Tags.Add(new HmiWebMetaData(assetName));
                            }
                        }

                        //Write the XML file & do cleanup ... but only if we have some valid information
                        if (retVal.Tags.Count > 0)
                        {
                            string saveError = retVal.Save(metadataFileName);
                            if (!string.IsNullOrEmpty(saveError))
                            {
                                errorMessages.Add(saveError);
                            }
                        }
                    }
                }
                catch (Exception)
                {
                    errorMessages.Add(string.Format("Error:\nFailed to parse the HMIWeb file '{0}'\nConsequences:\nSearch results for this graphic will not be available.", fullHmiWebGraphicFileName));
                }
            }

            return(retVal);
        }