context: // BlueBrick, a LEGO(c) layout editor.
  // Copyright (C) 2008 Alban NANTY
  //
  // 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, version 3 of the License.
  // see http://www.fsf.org/licensing/licenses/gpl.html
  // and http://www.gnu.org/licenses/
  //
  // 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.
  
  using System;
  using System.Collections.Generic;
  using System.Text;
  using System.Drawing;
  using System.Windows.Forms;
  using System.Xml.Serialization;
  using System.IO;
  using System.Collections;
  using BlueBrick.Actions;
  using BlueBrick.Actions.Bricks;
  using BlueBrick.Actions.Layers;
  using BlueBrick.Actions.Maps;
  using BlueBrick.Properties;
  
  namespace BlueBrick.MapData
  {
  	/// <summary>
  	/// This class represent a map created/edited by this application.
  	/// </summary>
  	/// <remarks>
  	/// <para>A Map contains several <see cref="Layer"/> stacked in a specific order.
  	/// The layers are displayed from the bottom to the top.</para>
  	/// <para>This class correspond also to the file that is save/load by the
  	/// application.</para>
  	/// </remarks>
  	[Serializable]
  	public class Map : IXmlSerializable
  	{
  		public enum BrickAddability
  		{
  			YES,
  			NO_WRONG_LAYER_TYPE,
  			NO_BUDGET_EXCEEDED,
  			YES_AND_NO_TRIMMED_BY_BUDGET,
  			YES_AND_NO_REPLACEMENT_LIMITED_BY_BUDGET
  		}
  
  		public static Hashtable sHashtableForRulerAttachementRebuilding = new Hashtable(); // this hashtable contains all the bricks is used to recreate the attachement of rulers to bricks when loading
  
  		// the current version of the data this version of BlueBrick can read/write
  		private const int CURRENT_DATA_VERSION = 8;
  
  		// the current version of the data
  		private static int mDataVersionOfTheFileLoaded = CURRENT_DATA_VERSION;
  
          // for the current map
          private string mMapFileName = Properties.Resources.DefaultSaveFileName;
          private bool mIsMapNameValid = false;
  
  		// data global to the map that can change the user
  		private string mAuthor = BlueBrick.Properties.Settings.Default.DefaultAuthor;
  		private string mLUG = BlueBrick.Properties.Settings.Default.DefaultLUG;
  		private string mShow = BlueBrick.Properties.Settings.Default.DefaultShow;
  		private DateTime mDate = DateTime.Today;
  		private string mComment = "";
  		private Color mBackgroundColor = BlueBrick.Properties.Settings.Default.DefaultBackgroundColor;
  		private string mGeneralInfoWatermark = "";
  
  		// data for the image export (this contains the last export settings for this map)
  		private string mExportAbsoluteFileName = string.Empty; // file name including full path from root
          private int mExportFileTypeIndex = 1; // index in the combobox for the different type of export
  		private RectangleF mExportArea = new RectangleF();
  		private double mExportScale = 0.0;
          private bool mExportWatermark = BlueBrick.Properties.Settings.Default.UIExportWatermark;
          private bool mExportBrickHull = BlueBrick.Properties.Settings.Default.UIExportHulls;
          private bool mExportElectricCircuit = BlueBrick.Properties.Settings.Default.UIExportElectricCircuit;
          private bool mExportConnectionPoints = BlueBrick.Properties.Settings.Default.UIExportConnection;
  		private bool mHasExportSettingsChanged = false; // a boolean flag indicating that the settings has changed and that the file need to be saved
  
  		// some data for compatibility with Track designer
  		private bool mAllowElectricShortCuts = false;
  		private bool mAllowUnderground = false;
  		private bool mAllowSteps = false;
  		private bool mAllowSlopeMismatch = false;
  
  		// a counter of modification
  		private int mNumberOfModificationSinceLastSave = 0;
  
  		// layer
  		private List<Layer> mLayers = new List<Layer>();
  
  		private Layer mSelectedLayer = null;
  		private Layer mLayerThatHandleMouse = null;
  
  		// singleton on the map (we assume it is always valid)
  		private static Map sInstance = new Map();
  
          // drawing data to draw the general info watermark
          private static SolidBrush mWatermarkBrush = new SolidBrush(Settings.Default.WatermarkColor);
          private static SolidBrush mWatermarkBackgroundBrush = new SolidBrush(Settings.Default.WatermarkBackgroundColor);
  
  		#region get/set
  
  		/// <summary>
  		/// The static instance of the map (this application is a single
  		/// doc application, so we can only have one map at the same time)
  		/// </summary>
  		public static Map Instance
  		{
  			get { return sInstance; }
  			set	{ sInstance = value; }
  		}
  
          public string MapFileName
          {
              get { return mMapFileName; }
              set { mMapFileName = value; }
          }
  
          public bool IsMapNameValid
          {
              get { return mIsMapNameValid; }
              set { mIsMapNameValid = value; }
          }
  
  		public string Author
  		{
  			get { return mAuthor; }
  			set { mAuthor = value; computeGeneralInfoWatermark();  }
  		}
  
  		public string LUG
  		{
  			get { return mLUG; }
  			set { mLUG = value; computeGeneralInfoWatermark(); }
  		}
  
  		public string Show
  		{
  			get { return mShow; }
  			set { mShow = value; computeGeneralInfoWatermark(); }
  		}
  
  		public DateTime Date
  		{
  			get { return mDate; }
  			set { mDate = value; computeGeneralInfoWatermark(); }
  		}
  
  		public string Comment
  		{
  			get { return mComment; }
  			set { mComment = value; }
  		}
  
  		public string GeneralInfoWatermark
  		{
  			get { return mGeneralInfoWatermark; }
  		}
  
  		public Color BackgroundColor
  		{
  			get { return mBackgroundColor; }
  			set { mBackgroundColor = value; }
  		}
  
  		public bool AllowElectricShortCuts
  		{
  			get { return mAllowElectricShortCuts; }
  			set { mAllowElectricShortCuts = value; }
  		}
  
  		public bool AllowUnderground
  		{
  			get { return mAllowUnderground; }
  			set { mAllowUnderground = value; }
  		}
  
  		public bool AllowSteps
  		{
  			get { return mAllowSteps; }
  			set { mAllowSteps = value; }
  		}
  
  		public bool AllowSlopeMismatch
  		{
  			get { return mAllowSlopeMismatch; }
  			set { mAllowSlopeMismatch = value; }
  		}
  
  		public int NumLayers
  		{
  			get { return mLayers.Count; }
  		}
  
  		public List<Layer> LayerList
  		{
  			get { return mLayers; }
  		}
  
  		public bool WasModified
  		{
  			get { return ((mNumberOfModificationSinceLastSave != 0) || mHasExportSettingsChanged); }
  			set
  			{
  				// if the value is false (meaning we just saved the file), reset all the flags
  				if (!value)
  				{
  					mNumberOfModificationSinceLastSave = 0;
  					mHasExportSettingsChanged = false;
  				}
  			}
  		}
  
  		public static int DataVersionOfTheFileLoaded
  		{
  			get { return mDataVersionOfTheFileLoaded; }
  		}
  
  		/// <summary>
  		/// The current selected layer
  		/// </summary>
  		public Layer SelectedLayer
  		{
  			get { return mSelectedLayer; }
  			set
  			{
  				// clear the selection of the previous selected layer
  				if (mSelectedLayer != null)
  					mSelectedLayer.clearSelection();
  
  				// select the new layer
  				mSelectedLayer = value;
  
  				// according to the type of the layer selected, enable or disable some tool on the main form
  				if (mSelectedLayer != null)
  				{
  					if (mSelectedLayer is LayerGrid)
  						MainForm.Instance.enableToolbarButtonOnLayerSelection(false, false, false);
  					else if (mSelectedLayer is LayerArea)
  						MainForm.Instance.enableToolbarButtonOnLayerSelection(false, true, false);
  					else if (mSelectedLayer is LayerRuler)
  						MainForm.Instance.enableToolbarButtonOnLayerSelection(true, false, true);
  					else
  						MainForm.Instance.enableToolbarButtonOnLayerSelection(true, false, false);
  				}
  				else
  				{
  					MainForm.Instance.enableToolbarButtonOnLayerSelection(false, false, false);
  				}
  			}
  		}
  
          public string ExportAbsoluteFileName
  		{
  			get { return mExportAbsoluteFileName; }
  		}
  
          public int ExportFileTypeIndex
  		{
  			get { return mExportFileTypeIndex; }
  		}
  
  		public RectangleF ExportArea
  		{
  			get { return mExportArea; }
  		}
  
  		public double ExportScale
  		{
  			get { return mExportScale; }
  		}
  
          public bool ExportWatermark
          {
              get { return mExportWatermark; }
          }
  
          public bool ExportBrickHull
          {
              get { return mExportBrickHull; }
          }
  
          public bool ExportElectricCircuit
          {
              get { return mExportElectricCircuit; }
          }
  
          public bool ExportConnectionPoints
          {
              get { return mExportConnectionPoints; }
          }
  		#endregion
  
  		#region constructor
  		/// <summary>
  		/// Constructor.
  		/// </summary>
  		public Map()
  		{
  			// check if the default name for Author, LUG and Show is empty in the settings
  			// then we take the ones for the resources
  			if (mAuthor.Equals("***NotInitialized***"))
  				mAuthor = BlueBrick.Properties.Resources.DefaultAuthor;
  			if (mLUG.Equals("***NotInitialized***"))
  				mLUG = BlueBrick.Properties.Resources.DefaultLUG;
  			if (mShow.Equals("***NotInitialized***"))
  				mShow = BlueBrick.Properties.Resources.DefaultShow;
  			// and construct the watermark
  			computeGeneralInfoWatermark();
  		}
  
  		/// <summary>
  		/// Update the gamma setting for the selections of bricks in brick layers
  		/// </summary>
  		public void updateGammaFromSettings()
  		{
  			foreach (Layer layer in mLayers)
  			{
  				LayerBrick brickLayer = layer as LayerBrick;
  				if (brickLayer != null)
  					brickLayer.updateGammaFromSettings();
  			}
  		}
  
  		/// <summary>
  		/// Compute the string displayed on top of the map from some general infos
  		/// </summary>
  		private void computeGeneralInfoWatermark()
  		{
  			mGeneralInfoWatermark = this.Author + ", " + this.LUG + ", " + this.Show + " (" + this.Date.ToShortDateString() + ")";
  		}
  		#endregion
  
  		#region IXmlSerializable Members
  
  		public System.Xml.Schema.XmlSchema GetSchema()
  		{
  			return null;
  		}
  
  		public void ReadXml(System.Xml.XmlReader reader)
  		{
  			// reset the counter of modifications because we just load the map (no modification done)
  			mNumberOfModificationSinceLastSave = 0;
  			// version
  			readVersionNumber(reader);
  			// check if the BlueBrick program is not too old, that
  			// means the user try to load a file generated with
  			/// a earlier version of BlueBrick
  			if (mDataVersionOfTheFileLoaded > CURRENT_DATA_VERSION)
  			{
  				MessageBox.Show(null, Properties.Resources.ErrorMsgProgramObsolete,
  					Properties.Resources.ErrorMsgTitleError, MessageBoxButtons.OK,
  					MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);				
  				return;
  			}
  			// get the number of layer for the progressbar
  			if (mDataVersionOfTheFileLoaded >= 3)
  			{
  				int nbItems = reader.ReadElementContentAsInt();
  				// init the progress bar with the real number of layer items (+1 for the header +1 for the link rebuilding)
  				MainForm.Instance.resetProgressBar(nbItems + 2);
  			}
  			// check is there is a background color
  			if (reader.Name.Equals("BackgroundColor"))
  				mBackgroundColor = XmlReadWrite.readColor(reader);
  			// data of the map
  			mAuthor = reader.ReadElementContentAsString();
  			mLUG = reader.ReadElementContentAsString();
  			mShow = reader.ReadElementContentAsString();
  			reader.ReadToDescendant("Day");
  			int day = reader.ReadElementContentAsInt();
  			int month = reader.ReadElementContentAsInt();
  			int year = reader.ReadElementContentAsInt();
  			mDate = new DateTime(year, month, day);
  			// read the comment if the version is greater than 0
  			if (mDataVersionOfTheFileLoaded > 0)
  			{
  				reader.ReadToFollowing("Comment");
                  mComment = reader.ReadElementContentAsString().Replace("\n", Environment.NewLine);
  			}
  			else
  			{
  				reader.ReadToFollowing("CurrentSnapGridSize");
  			}
  			if (mDataVersionOfTheFileLoaded < 2)
  			{
  				// skip the static data of layers that before we were saving
  				// but now I think it is stupid, since we don't have action to change that
  				// and we don't have way to update the enabled of the buttons
  				reader.ReadElementContentAsFloat(); // CurrentSnapGridSize
  				reader.ReadElementContentAsBoolean(); // SnapGridEnabled
  				reader.ReadElementContentAsFloat(); // CurrentRotationStep
  			}
  
              // read the export data if the version is 5 or higher
              if (mDataVersionOfTheFileLoaded > 4)
              {
                  reader.ReadToDescendant("ExportPath");
                  // read the relative export path and store it temporarly in the absolute path variable
                  // the absolute path will be computed after the xml serialization is finished
                  mExportAbsoluteFileName = reader.ReadElementContentAsString();
                  // read the other export info
                  mExportFileTypeIndex = reader.ReadElementContentAsInt();
                  mExportArea = XmlReadWrite.readRectangleF(reader);
                  mExportScale = reader.ReadElementContentAsFloat();
                  // read even more info from version 8
                  if (mDataVersionOfTheFileLoaded > 7)
                  {
                      mExportWatermark = XmlReadWrite.readBoolean(reader);
                      mExportBrickHull = XmlReadWrite.readBoolean(reader);
                      mExportElectricCircuit = XmlReadWrite.readBoolean(reader);
                      mExportConnectionPoints = XmlReadWrite.readBoolean(reader);
                  }
                  reader.ReadEndElement();
              }
  
  			// selected layer
  			int selectedLayerIndex = reader.ReadElementContentAsInt();
  			// step the progress bar after the read of the header
  			MainForm.Instance.stepProgressBar();
  
  			// layers
  			// first clear the hashtable that contains all the bricks
  			Map.sHashtableForRulerAttachementRebuilding.Clear();
  			// then load all the layers
  			bool layerFound = reader.ReadToDescendant("Layer");
  			while (layerFound)
  			{
  				// get the 'type' attribute of the layer
  				reader.ReadAttributeValue();
  				string layerType = reader.GetAttribute(0);
  
  				// instantiate the right layer according to the type
  				Layer layer = null;
  				if (layerType.Equals("grid"))
  					layer = new LayerGrid();
  				else if (layerType.Equals("brick"))
  					layer = new LayerBrick();
  				else if (layerType.Equals("text"))
  					layer = new LayerText();
  				else if (layerType.Equals("area"))
  					layer = new LayerArea();
  				else if (layerType.Equals("ruler"))
  					layer = new LayerRuler();
  
  				// read and add the new layer
  				if (layer != null)
  				{
  					layer.ReadXml(reader);
  					mLayers.Add(layer);
  				}
  
  				// read the next layer
  				layerFound = reader.ReadToNextSibling("Layer");
  			}
  			reader.ReadEndElement(); // end of Layers
  
  			// once we have finished to read all the layers thus all the items, we need to recreate all the links they have between them
  			foreach (Layer layer in mLayers)
  				layer.recreateLinksAfterLoading();
  			// then clear again the hash table to free the memory
  			Map.sHashtableForRulerAttachementRebuilding.Clear();
  
  			// step the progress bar after the rebuilding of links
  			MainForm.Instance.stepProgressBar();
  
  			// if the selected index is valid, reset the selected layer
  			// use the setter in order to enable the toolbar buttons
  			if ((selectedLayerIndex >= 0) && (selectedLayerIndex < mLayers.Count))
  				SelectedLayer = mLayers[selectedLayerIndex];
  			else
  				SelectedLayer = null;
  
  			// DO NOT READ YET THE BRICK URL LIST, BECAUSE THE BRICK DOWNLOAD FEATURE IS NOT READY
  			if (false)
  			{
  				// read the url of all the parts for version 5 or later
  				if ((mDataVersionOfTheFileLoaded > 5) && !reader.IsEmptyElement)
  				{
  					bool urlFound = reader.ReadToDescendant("BrickUrl");
  					while (urlFound)
  					{
  						// read the next url
  						urlFound = reader.ReadToNextSibling("BrickUrl");
  					}
  					reader.ReadEndElement();
  				}
  			}
  
  			// construct the watermark
  			computeGeneralInfoWatermark();
  			// for old version, make disapear the progress bar, since it was just an estimation
  			MainForm.Instance.finishProgressBar();
  		}
  
  		public void WriteXml(System.Xml.XmlWriter writer)
  		{
  			// reset the counter of modifications because we just save the map
  			mNumberOfModificationSinceLastSave = 0;
  
  			// first of all the version, we don't use the vesion read from the file,
  			saveVersionNumber(writer);
  
  			// write the number of items
  			int nbItems = 0;
  			foreach (Layer layer in mLayers)
  				nbItems += layer.NbItems;
  			writer.WriteElementString("nbItems", nbItems.ToString());
  			// init the progress bar
  			MainForm.Instance.resetProgressBar(nbItems + 1);
  
  			// the background color
  			XmlReadWrite.writeColor(writer, "BackgroundColor", mBackgroundColor);
  
  			// data of the map
  			writer.WriteElementString("Author", mAuthor);
  			writer.WriteElementString("LUG", mLUG);
  			writer.WriteElementString("Show", mShow);
  			writer.WriteStartElement("Date");
  				writer.WriteElementString("Day", mDate.Day.ToString());
  				writer.WriteElementString("Month", mDate.Month.ToString());
  				writer.WriteElementString("Year", mDate.Year.ToString());
  			writer.WriteEndElement();
  			writer.WriteElementString("Comment", mComment);
  
              // the export data
              string exportRelativeFileName = computeRelativePath(mMapFileName, mExportAbsoluteFileName);
              writer.WriteStartElement("ExportInfo");
                  writer.WriteElementString("ExportPath", exportRelativeFileName);
                  writer.WriteElementString("ExportFileType", mExportFileTypeIndex.ToString());
  				XmlReadWrite.writeRectangleF(writer, "ExportArea", mExportArea);
                  writer.WriteElementString("ExportScale", mExportScale.ToString(System.Globalization.CultureInfo.InvariantCulture));
                  XmlReadWrite.writeBoolean(writer, "ExportWatermark", mExportWatermark);
                  XmlReadWrite.writeBoolean(writer, "ExportHull", mExportBrickHull);
                  XmlReadWrite.writeBoolean(writer, "ExportElectricCircuit", mExportElectricCircuit);
                  XmlReadWrite.writeBoolean(writer, "ExportConnectionPoints", mExportConnectionPoints);
              writer.WriteEndElement();
  
  			// selected layer index
  			int selectedLayerIndex = -1;
  			if (mSelectedLayer != null)
  				selectedLayerIndex = mLayers.IndexOf(mSelectedLayer);
  			writer.WriteElementString("SelectedLayerIndex", selectedLayerIndex.ToString());
  
  			// step the progressbar after the header
  			MainForm.Instance.stepProgressBar();
  
  			// and the layer list
  			writer.WriteStartElement("Layers");
  			foreach (Layer layer in mLayers)
  				layer.WriteXml(writer);
  			writer.WriteEndElement(); // end of Layers
  
  			// DO NOT WRITE YET THE BRICK URL LIST, BECAUSE THE BRICK DOWNLOAD FEATURE IS NOT READY
  			if (false)
  			{
  				// write the brick url for all the bricks in the map
  				writer.WriteStartElement("BrickURLList");
  				// we use a hastable for fast hash search
  				System.Collections.Hashtable partList = new System.Collections.Hashtable();
  				foreach (Layer layer in mLayers)
  					if (layer is LayerBrick)
  						foreach (LayerBrick.Brick brick in (layer as LayerBrick).BrickList)
  							if (!partList.ContainsKey(brick.PartNumber))
  							{
  								// add the part in the hash map
  								partList.Add(brick.PartNumber, null);
  								// get the part from the Brick Library
  								string url = BrickLibrary.Instance.getImageURL(brick.PartNumber);
  								// serialize its url
  								if (url != null)
  								{
  									writer.WriteStartElement("BrickURL");
  									writer.WriteAttributeString("id", brick.PartNumber);
  									writer.WriteAttributeString("official", "yes");
  									writer.WriteString(url);
  									writer.WriteEndElement();
  								}
  							}
  				writer.WriteEndElement();
  			}
  		}
  
  		public void saveVersionNumber(System.Xml.XmlWriter writer)
  		{
  			// for saving we always save with the last version of data
  			writer.WriteElementString("Version", CURRENT_DATA_VERSION.ToString());
  		}
  
  		public void readVersionNumber(System.Xml.XmlReader reader)
  		{
  			reader.ReadToDescendant("Version");
  			mDataVersionOfTheFileLoaded = reader.ReadElementContentAsInt();
  		}
  		#endregion
  
  		#region update of map data
  
  		public void increaseModificationCounter()
  		{
  			++mNumberOfModificationSinceLastSave;
  		}
  
  		public void decreaseModificationCounter()
  		{
  			--mNumberOfModificationSinceLastSave;
  		}
  
          private string computeRelativePath(string fromFullPath, string toFullPath)
          {
              // both path should not be empty
              if (fromFullPath.Length == 0 || toFullPath.Length == 0)
                  return Path.GetFileName(toFullPath);
  
              // both path must be rooted, otherwise, we cannot compute the relative path
              if (!Path.IsPathRooted(fromFullPath) || !Path.IsPathRooted(toFullPath))
                  return Path.GetFileName(toFullPath);
  
              // if the two paths are not on the same drive this function cannot compute a relative path
              // for that we check the first letter. On Windows it will check the drive letter
              // on linux both path start with the directory separtor
              if (fromFullPath[0] != toFullPath[0])
                  return Path.GetFileName(toFullPath);
  
              // declare an array to store all the directory separator for all platform because one file
              // save on Windows could be open on Linux and vice and versa
              char[] separatorList = new char[] { Path.DirectorySeparatorChar, '/', '\\' };
  
              // cut the drive letter for windows, assuming on linux the function to get the root return an empty string
              fromFullPath = fromFullPath.Remove(0, Path.GetPathRoot(fromFullPath).Length);
              toFullPath = toFullPath.Remove(0, Path.GetPathRoot(toFullPath).Length);
              // remove also the first directory separator if any
              for (int i = 0; i < separatorList.Length; ++i)
              {
                  if (fromFullPath[0] == separatorList[i])
                      fromFullPath = fromFullPath.Remove(0, 1);
                  if (toFullPath[0] == separatorList[i])
                      toFullPath = toFullPath.Remove(0, 1);
              }
  
              // finally it seems the conditions are ok to compute a relative path
              // split the two paths in a serie of folders
              string[] fromFolders = fromFullPath.Split(separatorList);
              string[] toFolders = toFullPath.Split(separatorList);
  
              // iterate while we find the same folders
              int divergenceStartingIndex = 0;
              int maxIndex = Math.Min(fromFolders.Length, toFolders.Length);
  
gt:             for (int i = 0; i < maxIndex; ++i)