/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.data.relation;

import java.util.LinkedList;
import java.util.concurrent.Callable;

import org.geotools.api.data.FeatureSource;
import org.geotools.data.shapefile.dbf.DbaseFileReader;
import org.geotools.data.shapefile.files.ShpFiles;
import org.geotools.data.shapefile.shp.ShapefileReader;

import nz.org.riskscape.engine.RiskscapeException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.problem.Problems;

/**
 * Provides a simple single-threaded work queue to let us work around geotool's thread-bound locks for accessing
 * shapefiles. See code in {@link ShpFiles} for more info.
 *
 * Calling code can workaround the thread-per-lock constraint by requiring all lock-sensitive code to be called via
 * this 'defeater', e.g.
 *
 * ```
 * FeatureSource fs = lockDefeater.call(() -> dataStore.openFeatureSource());
 * // use features ... (doesn't need to be done via queue)
 * lockDefeater.call(() -> fs.close());
 * ```
 *
 *
 * https://gitlab.catalyst.net.nz/riskscape/riskscape/-/issues/307
 *
 * # An alternative...
 *
 * The whole reason that this has to exist is because of the locking assumptions in {@link ShpFiles} - if we can
 * remove the locking, then this issue is side-stepped (our code doesn't assume a multi-user system).  One way that
 * might remove some of the clutter and potentially be more efficient would be to go direct from {@link ShapefileReader}
 * and {@link DbaseFileReader} to {@link Tuple} and 'cut out the middle-man' - the {@link FeatureSource} code.
 */
@Slf4j
public class LockDefeater {

  // list of pending tasks
  private final LinkedList<Task<?>> queue = new LinkedList<>();

  private boolean stopped;


  // our special thread that 'fools' the thread-bounded locks in ShpFiles
  private final Thread thread = new Thread(null, () -> {
    while (true) {
      Task<?> task = null;
      synchronized (queue) {
        if (queue.isEmpty()) {
          try {
            queue.wait();
          } catch (InterruptedException e) {
            if (stopped) {
              log.info("Received stop, returning");
              return;
            } else {
              log.error("Unexpected interrupt - {}", e);
            }
          }
        } else {
          task = queue.removeFirst();
        }
      }

      if (task != null) {
        task.run();
      }
    }
  });
  public LockDefeater(String name) {
    thread.setDaemon(true);
    thread.setName(name);
  }

  /**
   * Call some code in the lock defeater's thread.  Enqueues a task, waits for it to finish, then returns its result.
   * @param <T> return type
   * @param taskName name to use for debug/log output
   * @param callable code to call
   * @throws RuntimeException if the calleable throws any kind of exception, it will be wrapped in a RuntimeException
   * and thrown from this thread
   * @return the result from the callable
   */
  public <T> T call(String taskName, Callable<T> callable) {

    if (!thread.isAlive()) {
      // guard against 2 different worker tasks trying to start the thread at the exact same time
      synchronized(thread) {
        if (!thread.isAlive()) {
          thread.start();
          log.info("Started lock defeat thread {}", thread);
        }
      }
    }

    Task<T> task = new Task<>(taskName, callable);

    synchronized (queue) {
      queue.add(task);
      queue.notify();
    }

    try {
      log.info("Waiting for task {} to complete", taskName);
      task.join();
      log.info("... task {} is complete", taskName);
      return task.result;
    } catch (InterruptedException e) {
      throw new RuntimeException("Unexpected interrupt waiting for task to finish", e);
    }
  }

  public void stop() {

    if (!thread.isAlive()) {
      return;
    }

    log.info("Interrupting thread {}...", thread);
    stopped = true;
    thread.interrupt();
    try {
      log.info("Waiting for thread {} to stop...", thread);
      thread.join();
      log.info("...Done");
    } catch (InterruptedException e) {
      throw new RuntimeException("unexpected interrupt", e);
    }
  }

  @RequiredArgsConstructor
  private static class Task<T> {
    private final String name;
    private final Callable<T> callable;
    private boolean done = false;
    private T result = null;
    public Exception error;

    public void join() throws InterruptedException {
      while (true) {
        synchronized (this) {
          if (done) {
            return;
          }

          if (error != null) {
            throw new RiskscapeException(Problems.caught(error));
          }

          this.wait();
        }
      }
    }

    public void run() {
      T t;
      try {
        t = callable.call();
      } catch (Exception e) {
        synchronized (this) {
          error = e;
          this.notify();
          return;
        }
      }

      synchronized (this) {
        done = true;
        result = t;
        this.notify();
      }
    }
  }

}
