/*
 * Jalview - A Sequence Alignment Editor and Viewer (2.11.5.0)
 * Copyright (C) 2025 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.renderer;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

import org.testng.annotations.Test;

import jalview.datamodel.AlignmentAnnotation;
import jalview.datamodel.ColumnSelection;
import jalview.datamodel.ContactListI;
import jalview.datamodel.ContactRange;
import jalview.datamodel.Mapping;
import jalview.datamodel.SeqDistanceContactMatrix;
import jalview.datamodel.Sequence;
import jalview.datamodel.SequenceI;
import jalview.util.MapList;
import jalview.util.StringUtils;
import jalview.ws.datamodel.MappableContactMatrixI;

public class ContactGeometryTest
{
  @Test(groups = "Functional")
  public void testCoverageofRange()
  {
    // a really dumb test to make sure we really cover the requested pixel and
    // contactList range for any dimension of each
    for (int range = 12; range < 2000; range += 35)
    {
      StringBuilder sb = new StringBuilder();
      while (sb.length() < range)
      {
        sb.append("c");
      }
      SequenceI sq = new Sequence("a", sb.toString());
      MappableContactMatrixI cm = new SeqDistanceContactMatrix(range);
      AlignmentAnnotation cm_aan = sq.addContactList(cm);
      ContactListI cl = sq.getContactListFor(cm_aan, 10);
      assertNotNull(cl);
      for (int ht = range / 2; ht < range * 3; ht++)
      {
        ContactGeometry clgeom = new ContactGeometry(cl, ht);
        assertNotNull(clgeom.allSteps());
      }
    }
  }

  @Test(groups = "Functional")
  public void testContactGeometry()
  {
    SequenceI sq = new Sequence("a", "SSSQ");
    MappableContactMatrixI cm = new SeqDistanceContactMatrix(4);
    AlignmentAnnotation cm_aan = sq.addContactList(cm);
    checkConsistencyFor(sq, cm_aan);
    // Also check all is good when there's a sequence mapping involved
    MappableContactMatrixI newcm = cm.liftOver(sq,
            new Mapping(sq, new MapList(new int[]
            { 1, 4 }, new int[] { 1, 4 }, 1, 1)));
    AlignmentAnnotation mapped_cm = sq.addContactList(newcm);
    checkConsistencyFor(sq, mapped_cm);
  }

  // Do some asserts for a sequence and a contact matrix
  private void checkConsistencyFor(SequenceI sq, AlignmentAnnotation cm_aan)
  {
    int col = 1;
    ContactListI cl = sq.getContactListFor(cm_aan, col);
    assertNotNull(cl);
    assertEquals(cl.getContactHeight(), 4);

    // Map contacts 0 to 3 to a tiny range and check
    ContactGeometry testee = new ContactGeometry(cl, 2);
    assertEquals(testee.contacts_per_pixel, 2d);
    ContactGeometry.contactInterval lastInterval = testee.mapFor(1);
    assertEquals(lastInterval.cStart, 2);
    assertEquals(lastInterval.cEnd, 3);
    assertEquals(lastInterval.pStart, 1);
    assertEquals(lastInterval.pEnd, 1);
    ContactGeometry.contactInterval another = testee.mapFor(1, 2);
    assertEquals(lastInterval, another);
    // Also check for a big pixel range
    testee = new ContactGeometry(cl, 395);
    lastInterval = testee.mapFor(390, 395); // 395 is one over limit.
    assertNotNull(lastInterval);
    assertEquals(lastInterval.cEnd, 3);
    assertEquals(lastInterval.pEnd, 394);
    // Map contacts 0 to 3 to Pixels 0-9, 10-19, 20-29, 30-39
    testee = new ContactGeometry(cl, 40);

    // verify mapping from pixel to contacts

    // renderer thinks base 0 for pixel coordinates
    // contact coordinates are base 1
    for (int p = 0; p < 40; p++)
    {
      int expectC = (p / 10);
      int expectP = (expectC) * 10;
      ContactGeometry.contactInterval ci_at = testee.mapFor(p),
              ci_from = testee.mapFor(p, p);
      assertNotNull(ci_at);
      // mapFor and map should locate the same pixel window
      assertEquals(ci_at.cStart, expectC,
              "Different cStart at position " + p);
      assertEquals(ci_at.cEnd, expectC, "Different cEnd at position " + p);
      assertEquals(ci_at.pStart, expectP,
              "Different pStart at position " + p);
      assertEquals(ci_at.pEnd, expectP + 9,
              "Different pEnd at position " + p);

      assertEquals(ci_from, ci_at,
              "Different contactIntervals at position " + p);
      // also test getRangeFor
      ContactRange cr = cl.getRangeFor(ci_at.cStart, ci_at.cEnd);
      assertEquals(cr.getFrom_column(), cr.getTo_column());
      assertEquals((double) cr.getMean(),
              (double) Math.abs(col - cr.getFrom_column()),
              "Didn't resolve expected value at position " + p);
    }

    ContactGeometry.contactInterval ci_at0 = testee.mapFor(0);
    ContactGeometry.contactInterval ci_at9 = testee.mapFor(9);
    assertNotNull(ci_at9);

    assertEquals(ci_at0, ci_at9);

    // Adjacent cell
    ContactGeometry.contactInterval ci_at10 = testee.mapFor(10);
    assertNotNull(ci_at10);
    ContactGeometry.contactInterval ci_at11 = testee.mapFor(11);
    assertNotNull(ci_at11);

    assertEquals(ci_at11, ci_at10,
            "Off-by-one in ContactGeometry mapping.");

    assertNotEquals(ci_at0, ci_at10,
            "Expected adjacent cells to be not equal.");

    // verify adjacent window is mapped
    assertEquals(ci_at11.cStart, ci_at9.cStart + 1);

    assertEquals(ci_at9.cEnd + 1, ci_at11.cStart);
    assertEquals(ci_at9.cEnd + 1, ci_at11.cEnd);

    // verify interval/intersection
    // column selection is base 0
    ColumnSelection cs = new ColumnSelection();
    cs.addElement(2);

    boolean mask = false;
    do
    {
      assertFalse(testee.intersects(ci_at0, cs, null, mask));
      assertFalse(testee.intersects(ci_at11, cs, null, mask));
      assertTrue(testee.intersects(testee.mapFor(21), cs, null, mask));
      assertFalse(testee.intersects(testee.mapFor(31), cs, null, mask));
      cs.addElement(3);
      assertTrue(testee.intersects(testee.mapFor(31), cs, null, mask));
      cs.removeElement(2);
      assertFalse(testee.intersects(testee.mapFor(21), cs, null, mask));
      mask = !mask;
    } while (!mask);

  }
}
