/*
 * Jalview - A Sequence Alignment Editor and Viewer (2.11.2.6)
 * Copyright (C) 2023 The Jalview Authors
 * 
 * This file is part of Jalview.
 * 
 * Jalview 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, either version 3
 * of the License, or (at your option) any later version.
 *  
 * Jalview 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 * The Jalview Authors are detailed in the 'AUTHORS' file.
 */
package jalview.io.vamsas;

import jalview.bin.Console;
import jalview.datamodel.AlignedCodonFrame;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.DBRefEntry;
import jalview.datamodel.Mapping;
import jalview.datamodel.SequenceI;
import jalview.gui.Desktop;
import jalview.io.VamsasAppDatastore;

import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import uk.ac.vamsas.objects.core.AlignmentSequence;
import uk.ac.vamsas.objects.core.DataSet;
import uk.ac.vamsas.objects.core.Sequence;
import uk.ac.vamsas.objects.core.SequenceMapping;
import uk.ac.vamsas.objects.core.SequenceType;

/**
 * binds a vamsas sequence mapping object from the vamsas document to a maplist
 * object associated with a mapping in the Jalview model. We use the maplist
 * object because these are referred to both in the Mapping object associated
 * with a jalview.datamodel.DBRefEntry and in the array of
 * jalview.datamodel.AlCodonFrame objects that Jalview uses to propagate
 * sequence mapping position highlighting across the views.
 * 
 * @author JimP
 * 
 */
public class Sequencemapping extends Rangetype
{
  public Sequencemapping(VamsasAppDatastore datastore,
          SequenceMapping sequenceMapping)
  {
    super(datastore, sequenceMapping, jalview.util.MapList.class);
    doJvUpdate();
  }

  private SequenceType from;

  private DataSet ds;

  private Mapping mjvmapping;

  /**
   * create or update a vamsas sequence mapping corresponding to a jalview
   * Mapping between two dataset sequences
   * 
   * @param datastore
   * @param mjvmapping
   * @param from
   * @param ds
   */
  public Sequencemapping(VamsasAppDatastore datastore,
          jalview.datamodel.Mapping mjvmapping,
          uk.ac.vamsas.objects.core.SequenceType from,
          uk.ac.vamsas.objects.core.DataSet ds)
  {
    super(datastore, mjvmapping.getMap(), SequenceMapping.class);
    this.from = from;
    this.ds = ds;
    this.mjvmapping = mjvmapping;
    validate();
    doSync();
  }

  /**
   * local check that extant mapping context is valid
   */
  public void validate()
  {

    SequenceMapping sequenceMapping = (SequenceMapping) vobj;
    if (sequenceMapping == null)
    {
      return;
    }
    if (from != null && sequenceMapping.getLoc() != from)
    {
      Console.warn("Probable IMPLEMENTATION ERROR: " + from
              + " doesn't match the local mapping sequence.");
    }
    if (ds != null && sequenceMapping.is__stored_in_document()
            && sequenceMapping.getV_parent() != ds)
    {
      Console.warn("Probable IMPLEMENTATION ERROR: " + ds
              + " doesn't match the parent of the bound sequence mapping object.");
    }
  }

  public void addToDocument()
  {
    add(mjvmapping, from, ds);
  }

  public void addFromDocument()
  {
    add((SequenceMapping) vobj);
  }

  public void conflict()
  {
    conflict(mjvmapping, (SequenceMapping) vobj);

  }

  public void updateToDoc()
  {
    update(mjvmapping, (SequenceMapping) vobj);
  }

  public void updateFromDoc()
  {
    update((SequenceMapping) vobj, (jalview.datamodel.Mapping) jvobj);
  }

  private void conflict(Mapping mjvmapping, SequenceMapping sequenceMapping)
  {
    System.err.println("Conflict in update of sequenceMapping "
            + sequenceMapping.getVorbaId());
  }

  private void add(Mapping mjvmapping,
          uk.ac.vamsas.objects.core.SequenceType from, DataSet ds)
  {
    SequenceI jvto = mjvmapping.getTo();
    while (jvto.getDatasetSequence() != null)
    {
      jvto = jvto.getDatasetSequence();
    }
    SequenceType to = (SequenceType) getjv2vObj(jvto);
    if (to == null)
    {
      Console.warn(
              "FIXME NONFATAL - do a second update: Ignoring Forward Reference to seuqence not yet bound to vamsas seuqence object");
      return;
    }
    SequenceMapping sequenceMapping = new SequenceMapping();
    sequenceMapping.setLoc(from);
    sequenceMapping.setMap(to);
    boolean dnaToProt = false, sense = false;
    // ensure that we create a mapping with the correct sense
    if (((Sequence) sequenceMapping.getLoc()).getDictionary().equals(
            uk.ac.vamsas.objects.utils.SymbolDictionary.STANDARD_NA))
    {
      if (((Sequence) sequenceMapping.getMap()).getDictionary().equals(
              uk.ac.vamsas.objects.utils.SymbolDictionary.STANDARD_AA))
      {
        dnaToProt = true;
        sense = true;
      }
    }
    else
    {
      if (((Sequence) sequenceMapping.getMap()).getDictionary().equals(
              uk.ac.vamsas.objects.utils.SymbolDictionary.STANDARD_NA))
      {
        dnaToProt = true;
        sense = false;
      }
    }

    if (!dnaToProt)
    {
      Console.warn(
              "Ignoring Mapping - don't support protein to protein mapping in vamsas document yet.");
      return;
    }
    if (ds == null)
    {
      // locate dataset for storage of SequenceMapping
      if (sense)
      {
        ds = (DataSet) ((uk.ac.vamsas.client.Vobject) sequenceMapping
                .getLoc()).getV_parent();
      }
      else
      {
        ds = (DataSet) ((uk.ac.vamsas.client.Vobject) sequenceMapping
                .getMap()).getV_parent();
      }
    }
    if (sense)
    {
      this.initMapType(sequenceMapping, mjvmapping.getMap(), true);
    }
    else
    {
      this.initMapType(sequenceMapping, mjvmapping.getMap().getInverse(),
              true);
    }
    ds.addSequenceMapping(sequenceMapping);
    sequenceMapping.setProvenance(
            this.dummyProvenance("user defined coding region translation")); // TODO:
    // correctly
    // construct
    // provenance
    // based
    // on
    // source
    // of
    // mapping
    bindjvvobj(mjvmapping.getMap(), sequenceMapping);

    Console.debug(
            "Successfully created mapping " + sequenceMapping.getVorbaId());
  }

  // private void update(jalview.util.MapList mjvmapping,
  // SequenceMapping sequenceMapping)
  {
    Console.error("Not implemented: Jalview Update Alcodon Mapping:TODO!");
  }

  private void update(SequenceMapping sequenceMapping,
          jalview.datamodel.Mapping mjvmapping)
  {
    Console.error("Not implemented: Update DBRef Mapping from Jalview");
  }

  private void update(jalview.datamodel.Mapping mjvmapping,
          SequenceMapping sequenceMapping)
  {
    Console.error("Not implemented: Jalview Update Sequence DBRef Mapping");
  }

  /**
   * bind a SequenceMapping to a live AlCodonFrame element limitations:
   * Currently, jalview only deals with mappings between dataset sequences, and
   * even then, only between those that map from DNA to Protein.
   * 
   * @param sequenceMapping
   */
  private void add(SequenceMapping sequenceMapping)
  {
    Object mobj;
    SequenceI from = null, to = null;
    boolean dnaToProt = false, sense = false;
    Sequence sdloc = null, sdmap = null;
    if (sequenceMapping.getLoc() instanceof AlignmentSequence)
    {
      sdloc = (Sequence) ((AlignmentSequence) sequenceMapping.getLoc())
              .getRefid();
    }
    else
    {
      sdloc = ((Sequence) sequenceMapping.getLoc());
    }
    if (sequenceMapping.getMap() instanceof AlignmentSequence)
    {
      sdmap = (Sequence) ((AlignmentSequence) sequenceMapping.getMap())
              .getRefid();
    }
    else
    {
      sdmap = ((Sequence) sequenceMapping.getMap());
    }
    if (sdloc == null || sdmap == null)
    {
      Console.info("Ignoring non sequence-sequence mapping");
      return;
    }
    mobj = this.getvObj2jv(sdloc);
    if (mobj instanceof SequenceI)
    {
      from = (SequenceI) mobj;
    }
    mobj = this.getvObj2jv(sdmap);
    if (mobj instanceof SequenceI)
    {
      to = (SequenceI) mobj;
    }
    if (from == null || to == null)
    {

      Console.error(
              "Probable Vamsas implementation error : unbound dataset sequences involved in a mapping are being parsed!");
      return;
    }

    if (sdloc.getDictionary().equals(
            uk.ac.vamsas.objects.utils.SymbolDictionary.STANDARD_NA))
    {
      if (sdmap.getDictionary().equals(
              uk.ac.vamsas.objects.utils.SymbolDictionary.STANDARD_AA))
      {
        dnaToProt = true;
        sense = true;
      }
      // else {

      // }
    }
    else
    {
      if (sdmap.getDictionary().equals(
              uk.ac.vamsas.objects.utils.SymbolDictionary.STANDARD_NA))
      {
        dnaToProt = true;
        sense = false;
      }
    }
    // create mapping storage object and make each dataset alignment reference
    // it.
    AlignmentI dsLoc = (AlignmentI) getvObj2jv(sdloc.getV_parent());
    AlignmentI dsMap = (AlignmentI) getvObj2jv(sdmap.getV_parent());
    AlignedCodonFrame acf = new AlignedCodonFrame();

    if (dsLoc != null && dsLoc != dsMap)
    {
      dsLoc.addCodonFrame(acf);
    }
    if (dsMap != null)
    {
      dsMap.addCodonFrame(acf);
    }
    // create and add the new mapping to (each) dataset's codonFrame

    jalview.util.MapList mapping = null;
    if (dnaToProt)
    {
      if (!sense)
      {
        mapping = this.parsemapType(sequenceMapping, 1, 3); // invert sense
        mapping = new jalview.util.MapList(mapping.getToRanges(),
                mapping.getFromRanges(), mapping.getToRatio(),
                mapping.getFromRatio());
        acf.addMap(to, from, mapping);
      }
      else
      {
        mapping = this.parsemapType(sequenceMapping, 3, 1); // correct sense
        acf.addMap(from, to, mapping);
      }
    }
    else
    {
      mapping = this.parsemapType(sequenceMapping, 1, 1); // correct sense
      acf.addMap(from, to, mapping);
    }
    bindjvvobj(mapping, sequenceMapping);
    jalview.structure.StructureSelectionManager
            .getStructureSelectionManager(Desktop.instance)
            .registerMapping(acf);
    // Try to link up any conjugate database references in the two sequences
    // matchConjugateDBRefs(from, to, mapping);
    // Try to propagate any dbrefs across this mapping.

  }

  /**
   * Complete any 'to' references in jalview.datamodel.Mapping objects
   * associated with conjugate DBRefEntry under given mapping
   * 
   * @param from
   *          sequence corresponding to from reference for sequence mapping
   * @param to
   *          sequence correspondeing to to reference for sequence mapping
   * @param smap
   *          maplist parsed in same sense as from and to
   */
  private void matchConjugateDBRefs(SequenceI from, SequenceI to,
          jalview.util.MapList smap)
  {
    if (from.getDBRefs() == null && to.getDBRefs() == null)
    {
      if (Console.isDebugEnabled())
      {
        Console.debug("Not matching conjugate refs for " + from.getName()
                + " and " + to.getName());
      }
      return;
    }
    if (Console.isDebugEnabled())
    {
      Console.debug("Matching conjugate refs for " + from.getName()
              + " and " + to.getName());
    }
    List<DBRefEntry> fdb = from.getDBRefs();
    List<DBRefEntry> tdb = new ArrayList<DBRefEntry>(to.getDBRefs());
    int tdblen = to.getDBRefs().size();
    //
    //
    // YOWSER
    //
    // System.arraycopy(to.getDBRefs(), 0, tdb, 0, tdblen);
    //
    //
    //
    //
    Vector matched = new Vector();
    jalview.util.MapList smapI = smap.getInverse();
    for (int f = 0, fn = fdb.size(); f < fn; f++)
    {
      jalview.datamodel.DBRefEntry fe = fdb.get(f);
      jalview.datamodel.Mapping fmp = fe.getMap();
      boolean fmpnnl = (fmp != null);
      // if (fmpnnl && fmp.getTo()!=null)
      // {
      // Cache.debug("Not overwriting existing To reference in
      // "+fe);
      // continue;
      // }
      // smap from maps from fe.local to fe.map
      boolean smapfromlocal2fe = (fmpnnl) ? smap.equals(fmp.getMap())
              : false;
      // smap from maps from fe.map to fe.local.
      boolean smapfromfemap2local = (fmpnnl) ? smapI.equals(fmp.getMap())
              : false;
      for (int t = 0; t < tdblen; t++)
      {
        jalview.datamodel.DBRefEntry te = tdb.get(t);
        if (te != null)
        {
          if (fe.getSource().equals(te.getSource())
                  && fe.getAccessionId().equals(te.getAccessionId()))
          {
            jalview.datamodel.Mapping tmp = te.getMap();
            boolean tmpnnl = tmp != null;
            if (tmpnnl && tmp.getTo() != null)
            {

            }
            // smap to maps from te.local to te.map
            boolean smaptolocal2tm = (tmpnnl) ? smap.equals(tmp.getMap())
                    : false;
            // smap to maps from te.map to te.local
            boolean smaptotemap2local = (tmpnnl)
                    ? smapI.equals(fmp.getMap())
                    : false;
            if (smapfromlocal2fe && smaptotemap2local)
            {
              // smap implies mapping from to to from
              fmp.setTo(to);
              tmp.setTo(from);
            }
            else if (smapfromfemap2local && smaptolocal2tm)
            {
              fmp.setTo(to);
            }
          }

        }
      }
    }
  }
}
