/*
 * 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.analysis.scoremodels;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import jalview.api.analysis.ScoreModelI;
import jalview.api.analysis.SimilarityParamsI;
import jalview.datamodel.Alignment;
import jalview.datamodel.AlignmentI;
import jalview.datamodel.AlignmentView;
import jalview.datamodel.Sequence;
import jalview.datamodel.SequenceFeature;
import jalview.datamodel.SequenceI;
import jalview.gui.AlignFrame;
import jalview.gui.AlignViewport;
import jalview.gui.JvOptionPane;
import jalview.io.DataSourceType;
import jalview.io.FileLoader;
import jalview.math.MatrixI;

import java.util.Arrays;

import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

public class FeatureDistanceModelTest
{

  @BeforeClass(alwaysRun = true)
  public void setUpJvOptionPane()
  {
    JvOptionPane.setInteractiveMode(false);
    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
  }

  public static String alntestFile = "FER1_MESCR/72-76 DVYIL\nFER1_SPIOL/71-75 DVYIL\nFER3_RAPSA/21-25 DVYVL\nFER1_MAIZE/73-77 DVYIL\n";

  int[] sf1 = new int[] { 74, 74, 73, 73, 23, 23, -1, -1 };

  int[] sf2 = new int[] { -1, -1, 74, 75, -1, -1, 76, 77 };

  int[] sf3 = new int[] { -1, -1, -1, -1, -1, -1, 76, 77 };

  /**
   * <pre>
   * Load test alignment and add features to sequences: 
   *      FER1_MESCR FER1_SPIOL FER3_RAPSA FER1_MAIZE 
   *  sf1     X          X          X  
   *  sf2                X                     X 
   *  sf3                                      X
   * </pre>
   * 
   * @return
   */
  public AlignFrame getTestAlignmentFrame()
  {
    AlignFrame alf = new FileLoader(false)
            .LoadFileWaitTillLoaded(alntestFile, DataSourceType.PASTE);
    AlignmentI al = alf.getViewport().getAlignment();
    Assert.assertEquals(al.getHeight(), 4);
    Assert.assertEquals(al.getWidth(), 5);
    for (int i = 0; i < 4; i++)
    {
      SequenceI ds = al.getSequenceAt(i).getDatasetSequence();
      if (sf1[i * 2] > 0)
      {
        ds.addSequenceFeature(new SequenceFeature("sf1", "sf1", sf1[i * 2],
                sf1[i * 2 + 1], "sf1"));
      }
      if (sf2[i * 2] > 0)
      {
        ds.addSequenceFeature(new SequenceFeature("sf2", "sf2", sf2[i * 2],
                sf2[i * 2 + 1], "sf2"));
      }
      if (sf3[i * 2] > 0)
      {
        ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", sf3[i * 2],
                sf3[i * 2 + 1], "sf3"));
      }
    }
    alf.setShowSeqFeatures(true);
    alf.getFeatureRenderer().setVisible("sf1");
    alf.getFeatureRenderer().setVisible("sf2");
    alf.getFeatureRenderer().setVisible("sf3");
    alf.getFeatureRenderer().findAllFeatures(true);
    Assert.assertEquals(
            alf.getFeatureRenderer().getDisplayedFeatureTypes().size(), 3,
            "Number of feature types");
    assertTrue(alf.getCurrentView().areFeaturesDisplayed());
    return alf;
  }

  @Test(groups = { "Functional" })
  public void testFeatureScoreModel() throws Exception
  {
    AlignFrame alf = getTestAlignmentFrame();
    ScoreModelI sm = new FeatureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            alf.getCurrentView().getAlignPanel());
    alf.selectAllSequenceMenuItem_actionPerformed(null);

    MatrixI dm = sm.findDistances(alf.getViewport().getAlignmentView(true),
            SimilarityParams.Jalview);
    assertEquals(dm.getValue(0, 2), 0d,
            "FER1_MESCR (0) should be identical with RAPSA (2)");
    assertTrue(dm.getValue(0, 1) > dm.getValue(0, 2),
            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
  }

  @Test(groups = { "Functional" })
  public void testFeatureScoreModel_hiddenFirstColumn() throws Exception
  {
    AlignFrame alf = getTestAlignmentFrame();
    // hiding first two columns shouldn't affect the tree
    alf.getViewport().hideColumns(0, 1);
    ScoreModelI sm = new FeatureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            alf.getCurrentView().getAlignPanel());
    alf.selectAllSequenceMenuItem_actionPerformed(null);
    MatrixI dm = sm.findDistances(alf.getViewport().getAlignmentView(true),
            SimilarityParams.Jalview);
    assertEquals(dm.getValue(0, 2), 0d,
            "FER1_MESCR (0) should be identical with RAPSA (2)");
    assertTrue(dm.getValue(0, 1) > dm.getValue(0, 2),
            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
  }

  @Test(groups = { "Functional" })
  public void testFeatureScoreModel_HiddenColumns() throws Exception
  {
    AlignFrame alf = getTestAlignmentFrame();
    // hide columns and check tree changes
    alf.getViewport().hideColumns(3, 4);
    alf.getViewport().hideColumns(0, 1);
    // getName() can become static in Java 8
    ScoreModelI sm = new FeatureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            alf.getCurrentView().getAlignPanel());
    alf.selectAllSequenceMenuItem_actionPerformed(null);
    MatrixI dm = sm.findDistances(alf.getViewport().getAlignmentView(true),
            SimilarityParams.Jalview);
    assertEquals(dm.getValue(0, 2), 0d,
            "After hiding last two columns FER1_MESCR (0) should still be identical with RAPSA (2)");
    assertEquals(dm.getValue(0, 1), 0d,
            "After hiding last two columns FER1_MESCR (0) should now also be identical with SPIOL (1)");
    for (int s = 0; s < 3; s++)
    {
      assertTrue(dm.getValue(s, 3) > 0d,
              "After hiding last two columns "
                      + alf.getViewport().getAlignment().getSequenceAt(s)
                              .getName()
                      + "(" + s
                      + ") should still be distinct from FER1_MAIZE (3)");
    }
  }

  /**
   * Check findFeatureAt doesn't return contact features except at contact
   * points TODO:move to under the FeatureRendererModel test suite
   */
  @Test(groups = { "Functional" })
  public void testFindFeatureAt_PointFeature() throws Exception
  {
    String alignment = "a CCCCCCGGGGGGCCCCCC\n" + "b CCCCCCGGGGGGCCCCCC\n"
            + "c CCCCCCGGGGGGCCCCCC\n";
    AlignFrame af = new jalview.io.FileLoader(false)
            .LoadFileWaitTillLoaded(alignment, DataSourceType.PASTE);
    SequenceI aseq = af.getViewport().getAlignment().getSequenceAt(0);
    SequenceFeature sf = null;
    sf = new SequenceFeature("disulphide bond", "", 2, 5, Float.NaN, "");
    aseq.addSequenceFeature(sf);
    assertTrue(sf.isContactFeature());
    af.refreshFeatureUI(true);
    af.getFeatureRenderer().setAllVisible(Arrays.asList("disulphide bond"));
    Assert.assertEquals(
            af.getFeatureRenderer().getDisplayedFeatureTypes().size(), 1,
            "Should be just one feature type displayed");
    // step through and check for pointwise feature presence/absence
    Assert.assertEquals(
            af.getFeatureRenderer().findFeaturesAtColumn(aseq, 1).size(),
            0);
    // step through and check for pointwise feature presence/absence
    Assert.assertEquals(
            af.getFeatureRenderer().findFeaturesAtColumn(aseq, 2).size(),
            1);
    // step through and check for pointwise feature presence/absence
    Assert.assertEquals(
            af.getFeatureRenderer().findFeaturesAtColumn(aseq, 3).size(),
            0);
    // step through and check for pointwise feature presence/absence
    Assert.assertEquals(
            af.getFeatureRenderer().findFeaturesAtColumn(aseq, 4).size(),
            0);
    // step through and check for pointwise feature presence/absence
    Assert.assertEquals(
            af.getFeatureRenderer().findFeaturesAtColumn(aseq, 5).size(),
            1);
    // step through and check for pointwise feature presence/absence
    Assert.assertEquals(
            af.getFeatureRenderer().findFeaturesAtColumn(aseq, 6).size(),
            0);
  }

  @Test(groups = { "Functional" })
  public void testFindDistances() throws Exception
  {
    String seqs = ">s1\nABCDE\n>seq2\nABCDE\n";
    AlignFrame alf = new FileLoader().LoadFileWaitTillLoaded(seqs,
            DataSourceType.PASTE);
    SequenceI s1 = alf.getViewport().getAlignment().getSequenceAt(0);
    SequenceI s2 = alf.getViewport().getAlignment().getSequenceAt(1);

    /*
     * set domain and variant features thus:
     *     ----5
     *  s1 ddd..
     *  s1 .vvv.
     *  s1 ..vvv    
     *  s2 .ddd. 
     *  s2 vv..v
     *  The number of unshared feature types per column is
     *     20120 (two features of the same type doesn't affect score)
     *  giving an average (pairwise distance) of 5/5 or 1.0 
     */
    s1.addSequenceFeature(
            new SequenceFeature("domain", null, 1, 3, 0f, null));
    s1.addSequenceFeature(
            new SequenceFeature("variant", null, 2, 4, 0f, null));
    s1.addSequenceFeature(
            new SequenceFeature("variant", null, 3, 5, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("domain", null, 2, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("variant", null, 1, 2, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("variant", null, 5, 5, 0f, null));
    alf.setShowSeqFeatures(true);
    alf.getFeatureRenderer().findAllFeatures(true);

    ScoreModelI sm = new FeatureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            alf.getCurrentView().getAlignPanel());
    alf.selectAllSequenceMenuItem_actionPerformed(null);

    AlignmentView alignmentView = alf.getViewport().getAlignmentView(true);
    MatrixI distances = sm.findDistances(alignmentView,
            SimilarityParams.Jalview);
    assertEquals(distances.width(), 2);
    assertEquals(distances.height(), 2);
    assertEquals(distances.getValue(0, 0), 0d);
    assertEquals(distances.getValue(1, 1), 0d);

    assertEquals(distances.getValue(0, 1), 1d,
            "expected identical pairs. (check normalisation for similarity score)");
    assertEquals(distances.getValue(1, 0), 1d);
  }

  /**
   * Verify computed distances with varying parameter options
   */
  @Test(groups = "Functional")
  public void testFindDistances_withParams()
  {
    AlignFrame af = setupAlignmentView();
    AlignViewport viewport = af.getViewport();
    AlignmentView view = viewport.getAlignmentView(false);

    ScoreModelI sm = new FeatureDistanceModel();
    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
            af.alignPanel);

    /*
     * feature distance model always normalises by region width
     * gap-gap is always included (but scores zero)
     * the only variable parameter is 'includeGaps'
     */

    /*
     * include gaps
     * score = 3 + 3 + 0 + 2 + 3 + 2 = 13/6
     */
    SimilarityParamsI params = new SimilarityParams(true, true, true, true);
    MatrixI distances = sm.findDistances(view, params);
    assertEquals(distances.getValue(0, 0), 0d);
    assertEquals(distances.getValue(1, 1), 0d);
    assertEquals(distances.getValue(0, 1), 13d / 6); // should be 13d/6
    assertEquals(distances.getValue(1, 0), 13d / 6);

    /*
     * exclude gaps
     * score = 3 + 3 + 0 + 0 + 0 + 0 = 6/6
     */
    params = new SimilarityParams(true, true, false, true);
    distances = sm.findDistances(view, params);
    assertEquals(distances.getValue(0, 1), 6d / 6);// should be 6d/6
  }

  /**
   * <pre>
   * Set up
   *   column      1 2 3 4 5 6
   *        seq s1 F R - K - S
   *        seq s2 F S - - L
   *   s1 chain    c c   c   c
   *   s1 domain   d d   d   d
   *   s2 chain    c c     c
   *   s2 metal    m m     m
   *   s2 Pfam     P P     P
   *      scores:  3 3 0 2 3 2
   * </pre>
   * 
   * @return
   */
  protected AlignFrame setupAlignmentView()
  {
    /*
     * for now, using space for gap to match callers of
     * AlignmentView.getSequenceStrings()
     * may change this to '-' (with corresponding change to matrices)
     */
    SequenceI s1 = new Sequence("s1", "FR K S");
    SequenceI s2 = new Sequence("s2", "FS  L");

    s1.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 4, 0f, null));
    s1.addSequenceFeature(
            new SequenceFeature("domain", null, 1, 4, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("chain", null, 1, 3, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("metal", null, 1, 3, 0f, null));
    s2.addSequenceFeature(
            new SequenceFeature("Pfam", null, 1, 3, 0f, null));
    AlignmentI al = new Alignment(new SequenceI[] { s1, s2 });
    AlignFrame af = new AlignFrame(al, 300, 300);
    af.setShowSeqFeatures(true);
    af.getFeatureRenderer().findAllFeatures(true);
    return af;
  }

}
