context: #region LGPL Header
  // Copyright (C) 2009, Jackie Ng
  // https://github.com/jumpinjackie/fdotoolbox, jumpinjackie@gmail.com
  // 
  // This library is free software; you can redistribute it and/or
  // modify it under the terms of the GNU Lesser General Public
  // License as published by the Free Software Foundation; either
  // version 2.1 of the License, or (at your option) any later version.
  // 
  // This library 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
  // Lesser General Public License for more details.
  // 
  // You should have received a copy of the GNU Lesser General Public
  // License along with this library; if not, write to the Free Software
  // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  // 
  //
  // See license.txt for more/additional licensing information
  #endregion
  using FdoToolbox.Core.CoordinateSystems;
  using FdoToolbox.Core.CoordinateSystems.Transform;
  using FdoToolbox.Core.ETL.Operations;
  using FdoToolbox.Core.Feature;
  using FdoToolbox.Core.Utility;
  using OSGeo.FDO.Schema;
  using OSGeo.MapGuide;
  using System;
  using System.Collections.Generic;
  using System.Collections.Specialized;
  using System.Data.OleDb;
  using System.Linq;
  
  namespace FdoToolbox.Core.ETL.Specialized
  {
      /// <summary>
      /// A specialized ETL process that copies data from one features class to another
      /// </summary>
      public class FdoClassToClassCopyProcess : FdoSpecializedEtlProcess
      {
          /// <summary>
          /// Gets or sets the options.
          /// </summary>
          /// <value>The options.</value>
          public FdoClassCopyOptions Options { get; private set; }
  
          /// <summary>
          /// Gets or sets the report frequency.
          /// </summary>
          /// <value>The report frequency.</value>
          public int ReportFrequency { get; set; }
  
  
          /// <summary>
          /// Initializes a new instance of the <see cref="FdoClassToClassCopyProcess"/> class.
          /// </summary>
          /// <param name="options">The options.</param>
          public FdoClassToClassCopyProcess(FdoClassCopyOptions options)
          {
              this.Options = options;
          }
  
          /// <summary>
          /// Gets the name of this instance
          /// </summary>
          /// <value>The name.</value>
          public override string Name => string.IsNullOrEmpty(this.Options.Name) ?
                      string.Format("Copy features from {0} to {1}", this.Options.SourceClassName, this.Options.TargetClassName) :
                      this.Options.Name;
  
          struct CSTransform : IEquatable<CSTransform>
          {
              public CSTransform(string source, string target)
              {
                  this.Source = source;
                  this.Target = target;
              }
  
              public string Source { get; }
  
              public string Target { get; }
  
              public override bool Equals(object obj)
              {
                  return obj is CSTransform transform && Equals(transform);
              }
  
              public bool Equals(CSTransform other)
              {
                  return Source == other.Source &&
                         Target == other.Target;
              }
  
              public override int GetHashCode()
              {
                  int hashCode = -1031959520;
                  hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Source);
                  hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Target);
                  return hashCode;
              }
  
              public static bool operator ==(CSTransform left, CSTransform right)
              {
                  return left.Equals(right);
              }
  
              public static bool operator !=(CSTransform left, CSTransform right)
              {
                  return !(left == right);
              }
          }
  
          class ClassCopyContext
          {
              public CSTransform? Transform { get; set; }
          }
  
          class GeometryTransformOperation : FdoOperationBase
          {
              private FdoGeometryTransformOperation _innerXform;
              readonly ClassCopyContext _context;
  
              public GeometryTransformOperation(ClassCopyContext context)
              {
                  _context = context;
              }
  
              public override IEnumerable<FdoRow> Execute(IEnumerable<FdoRow> rows)
              {
                  if (_context.Transform.HasValue)
                  {
                      var xform = _context.Transform.Value;
                      _innerXform = new FdoGeometryTransformOperation(new FdoGeometryTransformingConverter(xform.Source, xform.Target));
                  }
  
                  if (_innerXform != null)
                  {
                      return _innerXform.Execute(rows);
                  }
                  else
                  {
                      return rows;
                  }
              }
          }
  
          /// <summary>
          /// If true, only the setup portion of the bulk copy is run (the actual copying
          /// of features will be skipped)
          /// </summary>
          public bool RunSetupOnly { get; set; }
  
          class PreClassCopyModifyOperation : FdoOperationBase
          {
              private FdoConnection _source;
              private FdoConnection _target;
              private FdoClassCopyOptions _opts;
              readonly NameValueCollection _sourceToTargetProps;
              readonly ClassCopyContext _context;
  
              public PreClassCopyModifyOperation(FdoClassCopyOptions opts,
                                                 FdoConnection source,
                                                 FdoConnection target,
                                                 NameValueCollection sourceToTargetProps,
                                                 ClassCopyContext context)
              {
                  if (opts.PreCopyTargetModifier == null)
                      throw new ArgumentException("No pre-copy modifier specified");
  
                  _source = source;
                  _target = target;
                  _opts = opts;
                  _sourceToTargetProps = sourceToTargetProps;
                  _context = context;
              }
  
              private int _counter = 0;
  
              public string UseTargetSpatialContext { get; internal set; }
  
              public override IEnumerable<FdoRow> Execute(IEnumerable<FdoRow> rows)
              {
                  if (_counter < 1) //Shouldn't be reentrant, but just play it safe.
                  {
                      /*
                       * Check and apply the following rules for all geometry properties to be created
                       * 
                       * Target supports multiple spatial contexts:
                       * -------------------------------------------
                       * If there is no spatial contexts of the specified (source) name. Create a copy of the source spatial context.
                       * If there is a spatial context of the same name, using the same WKT. Do nothing
                       * If there is a spatial context of the same name, but using a different WKT. Create a new spatial context using the source WKT, but use a different name.
                       * 
                       * Target only supports one spatial context:
                       * If there is no spatial context already. Create a copy of the source spatial context.
                       * If there is a spatial context of the same WKT. Change the association to match the name of this spatial context.
                       * If there is a spatial context not using the source WKT. Change the association to match the name of this spatial context. This may not be ideal, but there is no other option at this point.
                       * 
                       * The regular schema compatibility fixes will handle the other properties
                       */
  
                      bool targetSupportsMultipleSpatialContexts = false;
                      using (var connCaps = _target.ConnectionCapabilities)
                      {
                          targetSupportsMultipleSpatialContexts = connCaps.SupportsMultipleSpatialContexts();
                      }
                      List<SpatialContextInfo> targetSpatialContexts = null;
                      List<SpatialContextInfo> sourceSpatialContexts = null;
  
                      if (typeof(CreateTargetClassFromSource).IsAssignableFrom(_opts.PreCopyTargetModifier.GetType()))
                      {
                          using (var tsvc = _target.CreateFeatureService())
                          using (var ssvc = _source.CreateFeatureService())
                          {
                              targetSpatialContexts = new List<SpatialContextInfo>(tsvc.GetSpatialContexts());
                              sourceSpatialContexts = new List<SpatialContextInfo>(ssvc.GetSpatialContexts());
                              var ct = (CreateTargetClassFromSource)_opts.PreCopyTargetModifier;
  
                              Info("Getting current schema from target");
                              FeatureSchema schema = null;
                              try
                              {
                                  schema = tsvc.GetSchemaByName(_opts.TargetSchema);
                              }
                              catch (SchemaNotFoundException) { }
                              if (schema == null)
                              {
                                  Info($"Target schema ({_opts.TargetSchema}) not found. Creating it");
                                  schema = new FeatureSchema { Name = _opts.TargetSchema };
                                  tsvc.ApplySchema(schema);
                                  //Re-request so it's pure and free of any element states
                                  schema = tsvc.GetSchemaByName(_opts.TargetSchema);
                              }
  
                              var classes = schema.Classes;
                              if ((!string.IsNullOrWhiteSpace(_opts.TargetClassNameOverride) && classes.IndexOf(_opts.TargetClassNameOverride) >= 0) || classes.IndexOf(ct.Name) >= 0)
                              {
                                  Info("Class " + _opts.TargetSchema + ":" + (_opts.TargetClassNameOverride ?? ct.Name) + " already exists. Nothing to do here");
                              }
                              else
                              {
                                  List<SpatialContextInfo> createScs = new List<SpatialContextInfo>();
  
                                  var cls = ssvc.GetClassByName(ct.Schema, ct.Name);
                                  Info("Creating a cloned copy of source class " + ct.Schema + ":" + ct.Name);
  
                                  var cloned = FdoSchemaUtil.CloneClass(cls);
                                  if (!string.IsNullOrWhiteSpace(_opts.TargetClassNameOverride))
                                  {
                                      cloned.Name = _opts.TargetClassNameOverride;
                                      Info($"Class will be created as: {cloned.Name}");
                                  }
                                  var propList = new List<string>(_opts.CheckSourceProperties);
                                  var removeList = new List<string>();
                                  foreach (PropertyDefinition prop in cloned.Properties)
                                  {
                                      string propName = prop.Name;
                                      if (!propList.Contains(propName))
                                      {
                                          removeList.Add(propName);
                                      }
                                      else
                                      {
                                          var targetName = _sourceToTargetProps[prop.Name];
                                          if (targetName != null && prop.Name != targetName)
                                          {
                                              Info($"Renaming property ({prop.Name}) to ({targetName}) to match property mappings");
                                              prop.Name = targetName;
                                          }
                                      }
                                  }
  
                                  if (removeList.Count > 0)
                                  {
                                      Info("Removing " + removeList.Count + " unused properties from cloned class");
                                      var props = cloned.Properties;
                                      var ids = cloned.IdentityProperties;
                                      foreach (var name in removeList)
                                      {
                                          if (ids.Contains(name))
                                              ids.RemoveAt(ids.IndexOf(name));
  
                                          if (props.Contains(name))
                                              props.RemoveAt(props.IndexOf(name));
                                      }
                                      Info(removeList.Count + " unused properties removed");
                                  }
  
                                  foreach (var prop in ct.PropertiesToCreate)
                                  {
                                      Info("Adding property to cloned class: " + prop.Name);
                                      PropertyDefinition clonedProp = FdoSchemaUtil.CloneProperty(prop);
                                      var targetName = _sourceToTargetProps[prop.Name];
                                      if (targetName != null && clonedProp.Name != targetName)
                                      {
                                          clonedProp.Name = targetName;
                                          Info("Renaming property to: " + targetName);
                                      }
                                      cloned.Properties.Add(clonedProp);
                                  }
  
                                  //Add an auto-generated identity property if none exist
                                  if (cloned.IdentityProperties.Count == 0)
                                  {
                                      var id = new DataPropertyDefinition("FID", "Auto-Generated Feature Id")
                                      {
                                          IsAutoGenerated = true,
                                          Nullable = false,
                                          //This may not be valid for target connection, but FdoFeatureService
                                          //will fix this for us.
                                          DataType = DataType.DataType_Int32
                                      };
  
                                      cloned.Properties.Add(id);
                                      cloned.IdentityProperties.Add(id);
  
                                      Info("Adding an auto-generated id (FID) to this cloned class");
                                  }
  
                                  Info("Checking this class for incompatibilities");
                                  var handledGeomProps = new HashSet<string>();
                                  IncompatibleClass ic;
                                  if (!tsvc.CanApplyClass(cloned, out ic))
                                  {
                                      Info("Altering this class to become compatible with target connection");
                                      cloned = tsvc.AlterClassDefinition(cloned, ic, (geomProp, activeScInfo) =>
                                      {
                                          _context.Transform = AddSpatialContextsToCreate(targetSupportsMultipleSpatialContexts,
                                                                                          targetSpatialContexts,
                                                                                          sourceSpatialContexts,
                                                                                          createScs,
                                                                                          geomProp);
                                          //Register this prop as handled so we don't attempt to double fix the same property later on
                                          handledGeomProps.Add($"{cloned.Name}:{geomProp.Name}");
                                      });
                                      Info("Class successfully altered");
                                  }
  
                                  Info("Checking if any spatial contexts need to be created and/or references modified");
                                  foreach (PropertyDefinition pd in cloned.Properties)
                                  {
                                      if (pd.PropertyType == PropertyType.PropertyType_GeometricProperty && !handledGeomProps.Contains($"{cloned.Name}:{pd.Name}"))
                                      {
                                          _context.Transform = AddSpatialContextsToCreate(targetSupportsMultipleSpatialContexts,
                                                                                          targetSpatialContexts,
                                                                                          sourceSpatialContexts,
                                                                                          createScs,
                                                                                          (GeometricPropertyDefinition)pd);
                                      }
                                  }
  
                                  if (!string.IsNullOrWhiteSpace(this.UseTargetSpatialContext))
                                  {
                                      Info("Not creating any spatial contexts (elected to use target spatial context)");
                                      var clsProps = cls.Properties;
                                      foreach (PropertyDefinition pd in clsProps)
                                      {
                                          if (pd is GeometricPropertyDefinition gp)
                                          {
                                              if (gp.SpatialContextAssociation != this.UseTargetSpatialContext)
                                              {
                                                  gp.SpatialContextAssociation = this.UseTargetSpatialContext;
                                                  Info($"Applied target spatial context ({UseTargetSpatialContext}) for geometric property: {gp.Name}");
                                              }
                                          }
                                      }
                                  }
                                  else
                                  {
                                      //We have to create spatial contexts first before applying the schema
                                      if (createScs.Count > 0)
                                      {
                                          //The ones we create should be unique so no overwriting needed
                                          ExpressUtility.CopyAllSpatialContexts(createScs, _target, false);
  
                                          foreach (var sc in createScs)
                                          {
                                              Info("Created spatial context: " + sc.Name);
                                          }
                                      }
                                  }
  
                                  Info("Adding cloned class to target schema");
                                  schema.Classes.Add(cloned);
                                  Info("Applying schema back to target connection");
                                  var prv = _target.Provider.ToUpper();
                                  // HACK: Schemas won't apply in Oracle without explicitly ignoring states
                                  if (prv.Contains("KING") && prv.Contains("ORACLE"))
                                      tsvc.ApplySchema(schema, null, true);
                                  else
                                      tsvc.ApplySchema(schema);
                                  Info("Updated schema applied to target connection");
                              }
                          }
                      }
                      else if (typeof(UpdateTargetClass).IsAssignableFrom(_opts.PreCopyTargetModifier.GetType()))
                      {
                          var ut = (UpdateTargetClass)_opts.PreCopyTargetModifier;
                          using (var tsvc = _target.CreateFeatureService())
                          using (var ssvc = _source.CreateFeatureService())
                          {
                              targetSpatialContexts = new List<SpatialContextInfo>(tsvc.GetSpatialContexts());
                              sourceSpatialContexts = new List<SpatialContextInfo>(ssvc.GetSpatialContexts());
  
gt:                             var schema = tsvc.GetSchemaByName(_opts.TargetSchema);