/**
 * This class provides basic information about the state and location of the current Cyc server.
 *
 * @author nwinant
 */
public class CycServerInfoImpl implements CycServerInfo {

  // Fields

  /**
   * Functions which are so critical that API compatibility isn't even a possibility without them.
   *
   * <p>TODO: should these just be the set of required functions in SubLFunctions?
   */
  /*
  private static final List<SubLFunction> CRITICAL_FUNCTIONS = Arrays.<SubLFunction>asList(
          CYC_REVISION_NUMBERS,
          CYC_SYSTEM_CODE_STRING,
          CYC_REVISION_STRING,
          KB_VERSION_STRING
  );
  */
  private static final int REQUIRED_MAJOR_REVISION = 10;

  private static final int MINIMUM_MINOR_REVISION_MAINT = 152303;
  private static final int MINIMUM_MINOR_REVISION_RCYC = 152303;
  private static final int MINIMUM_MINOR_REVISION_ECYC = 157082;
  private static final int MINIMUM_MINOR_REVISION_OCYC = 158630;
  private static final String MINIMUM_RCYC_RELEASE = "4.0q";
  private static final String MINIMUM_ECYC_RELEASE = "1.7-preview";
  private static final String MINIMUM_OCYC_RELEASE = "5.0-preview";
  private static final String CYC_DOWNLOAD_URL = "http://dev.cyc.com/downloads/";

  private static final Logger LOGGER = LoggerFactory.getLogger(CycServerInfoImpl.class);

  /**
   * We currently have no way of knowing if this implementation will be compatible with systems
   * *later* than Cyc 10, but it doesn't seem likely. - nwinant, 2015-05-29
   */
  public static final AbstractCycClientRequirement MAJOR_REVISION_REQUIREMENT =
      new AbstractCycClientRequirement() {
        @Override
        public boolean isCompatible(CycClient client)
            throws CycApiException, CycConnectionException {
          final CycServerInfoImpl info = client.getServerInfo();
          return info.getCycMajorRevisionNumber() == REQUIRED_MAJOR_REVISION;
        }

        @Override
        public String getErrorMessage(CycClient client)
            throws UnsupportedCycOperationException, CycApiException, CycConnectionException {
          final int serverMajorRevisionNumber = client.getServerInfo().getCycMajorRevisionNumber();
          final CycServer server = client.getCycServer();
          return "This API implementation requires servers at System "
              + REQUIRED_MAJOR_REVISION
              + ", but "
              + server
              + " is at System "
              + serverMajorRevisionNumber
              + ".";
        }
      };

  public static final CycClientRequirementList BASELINE_API_REQUIREMENTS =
      CycClientRequirementList.fromList(
          new ServerFunctionRequirement(CYC_REVISION_NUMBERS),
          MAJOR_REVISION_REQUIREMENT,
          new ServerFunctionRequirement(CYC_SYSTEM_CODE_STRING),
          new ServerFunctionRequirement(CYC_REVISION_STRING),
          new ServerFunctionRequirement(KB_VERSION_STRING),
          new MinimumPatchRequirement(CycServerReleaseType.MAINT, MINIMUM_MINOR_REVISION_MAINT),
          new MinimumPatchRequirement(
              CycServerReleaseType.RESEARCHCYC, MINIMUM_MINOR_REVISION_RCYC),
          new MinimumPatchRequirement(
              CycServerReleaseType.ENTERPRISECYC, MINIMUM_MINOR_REVISION_ECYC),
          new MinimumPatchRequirement(CycServerReleaseType.OPENCYC, MINIMUM_MINOR_REVISION_OCYC));

  private final CycAccess cyc;
  private final CycServer server;
  private boolean openCyc;
  private List<Integer> cycRevisionNumbers;

  // Constructors

  public CycServerInfoImpl(CycAccess cyc) {
    this.cyc = cyc;
    if ((cyc != null) && !cyc.isClosed()) {
      this.server = cyc.getCycServer();
    } else {
      server = null;
    }
    try {
      reloadCache();
    } catch (SessionCommunicationException ex) {
      throw new SessionApiRuntimeException(ex);
    }
  }

  // Public

  public final void reloadCache() throws SessionCommunicationException {
    try {

      // TODO: add more caching of server info values - nwinant, 2015-06-25

      this.openCyc = this.getCyc().isOpenCyc(); // TODO: this is already cached inside CycClient.
      this.cycRevisionNumbers = CYC_REVISION_NUMBERS.eval(getCyc());
    } catch (CycConnectionException ex) {
      ex.throwAsSessionException();
    }
  }

  @Override
  public CycServer getCycServer() {
    return this.server;
  }

  /**
   * Returns the browser URL for the Cyc image that this CycAccess is connected to, as the URL would
   * be seen from the machine where the CycAccess is running. Note that if there is a firewall,
   * port-forwarding or other indirection between the browser and the machine where this CycSession
   * is situated, the URL returned may not be functional.
   */
  @Override
  public String getBrowserUrl() {
    return getBaseBrowserUrl() + "/cgi-bin/cg?cb-start";
  }

  public String getBaseBrowserUrl() {
    return "http://" + getCycServer().getResolvedHostName() + ":" + getCycServer().getHttpPort();
  }

  /**
   * Returns the Cyc revision string (akin to the build number).
   *
   * @return the Cyc revision string for the Cyc server.
   * @throws SessionCommunicationException if a communications error occurs or the Cyc server cannot
   *     be found.
   * @throws SessionCommandException if an error occurs in issuing a command to the Cyc server.
   */
  @Override
  public String getCycRevisionString()
      throws SessionCommunicationException, SessionCommandException {
    try {
      return CYC_REVISION_STRING.eval(getCyc());
    } catch (CycConnectionException ex) {
      ex.throwAsSessionException();
    } catch (CycApiException ex) {
      ex.throwAsSessionException();
    }
    return null;
  }

  /**
   * Returns the KB version string (KBNum.OperationCount) for the Cyc server.
   *
   * @return the Cyc KB version string.
   * @throws SessionCommunicationException if a communications error occurs or the Cyc server cannot
   *     be found.
   * @throws SessionCommandException if an error occurs in issuing a command to the Cyc server.
   */
  @Override
  public String getCycKBVersionString()
      throws SessionCommunicationException, SessionCommandException {
    try {
      return KB_VERSION_STRING.eval(getCyc());
    } catch (CycConnectionException ex) {
      ex.throwAsSessionException();
    } catch (CycApiException ex) {
      ex.throwAsSessionException();
    }
    return null;
  }

  @Override
  public boolean isAPICompatible() throws SessionCommunicationException, SessionCommandException {
    try {
      //    return isExecutableAPICompatible() && isKBAPICompatible();
      return isExecutableAPICompatible();
    } catch (CycConnectionException ex) {
      ex.throwAsSessionException();
    } catch (CycApiException ex) {
      ex.throwAsSessionException();
    }
    return false;
  }

  @Override
  public boolean isOpenCyc() throws SessionCommunicationException {
    return this.openCyc;
  }

  @Override
  public CycServerReleaseType getSystemReleaseType()
      throws SessionCommunicationException, SessionCommandException {
    return CycServerReleaseType.fromString(getSystemReleaseTypeString());
  }

  public int getCycMajorRevisionNumber() throws CycConnectionException, CycApiException {
    return getCycRevisionNumbers().get(0);
  }

  @Override
  public int getCycMinorRevisionNumber()
      throws SessionCommunicationException, SessionCommandException {
    return getCycRevisionNumbers().get(1);
  }

  // Protected

  protected String getSystemReleaseTypeString()
      throws SessionCommunicationException, SessionCommandException {
    try {
      return CYC_SYSTEM_CODE_STRING.eval(getCyc());
    } catch (CycConnectionException ex) {
      ex.throwAsSessionException();
    } catch (CycApiException ex) {
      ex.throwAsSessionException();
    }
    return null;
  }

  protected CycAccess getCyc() {
    return this.cyc;
  }

  // Internal
  /*
  private int checkForMissingCriticalFunctions() throws CycApiException, CycConnectionException {
    int i = 0;
    for (SubLFunction function : CRITICAL_FUNCTIONS) {
      if (!function.isBound(getCyc())) {
        LOGGER.error("{} is missing critical function: {}", server, function);
        i++;
      }
    }
    return i;
  }
  */

  private boolean isExecutableAPICompatible()
      throws CycConnectionException, CycApiException, SessionCommunicationException,
          SessionCommandException {

    /*
    if (checkForMissingCriticalFunctions() > 0) {
      return false;
    }
    */
    /*
    // We currently have no way of knowing if this implementation will be compatible with systems
    // *later* than Cyc 10, but it doesn't seem likely. - nwinant, 2015-05-29
    final int serverMajorRevisionNumber = getCycMajorRevisionNumber();
    if (getCycMajorRevisionNumber() != REQUIRED_MAJOR_REVISION) {
      LOGGER.error("This API implementation requires servers at System {}, but {} is at System {}.", REQUIRED_MAJOR_REVISION, server, serverMajorRevisionNumber);
      return false;
    }
    */
    return BASELINE_API_REQUIREMENTS.isCompatible((CycClient) getCyc());

    // TODO: clean up this cruft - nwinant, 2015-06-25

    /*
    if (!BASELINE_API_REQUIREMENTS.isCompatible(getCyc().getCycSession())) {
      return false;
    }*/

    /*
    final int serverMinorRevisionNumber = getCycMinorRevisionNumber();
    final CycServerReleaseType type = getSystemReleaseType();
    final Integer minimumMinorRevisionNumber;
    final String minimumReleaseNumber;
    final String downloadUrl;
    switch (type) {
      case MAINT:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_MAINT;
        minimumReleaseNumber = null;
        downloadUrl = null;
        break;

      case RESEARCHCYC:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_RCYC;
        minimumReleaseNumber = MINIMUM_RCYC_RELEASE;
        downloadUrl = CYC_DOWNLOAD_URL;
        break;

      case ENTERPRISECYC:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_ECYC;
        minimumReleaseNumber = MINIMUM_ECYC_RELEASE;
        downloadUrl = CYC_DOWNLOAD_URL;
        break;

      case OPENCYC:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_OCYC;
        minimumReleaseNumber = MINIMUM_OCYC_RELEASE;
        downloadUrl = CYC_DOWNLOAD_URL;
        break;

      default:
        LOGGER.error("Unknown system type '{}' for {}, cannot determine compatibility.", getSystemReleaseTypeString(), server);
        return false;
    }

    if (serverMinorRevisionNumber < minimumMinorRevisionNumber) {
      LOGGER.error("{} servers must be at least System {}.{} for API compatibility, but {} is only at System {}.{}.",
              type.getName(),
              serverMajorRevisionNumber, minimumMinorRevisionNumber,
              server,
              serverMajorRevisionNumber, serverMinorRevisionNumber);
      if (minimumReleaseNumber != null) {
        LOGGER.error("The earliest release of {} supported by this API implementation is {} {}.",
                type.getName(),
                type.getName(),
                minimumReleaseNumber);
      }
      if (downloadUrl != null) {
        LOGGER.error("To upgrade your {} server, visit {}.", type.getName(), downloadUrl);
      }
      return false;
    }
    return true;
    */
  }

  /*
  private boolean isKBAPICompatible() throws CycApiException, CycConnectionException {
    try {
      return Double.parseDouble(getCycKBVersionString()) >= MINIMUM_KB_VERSION;
    } catch (NumberFormatException nfe) {
      nfe.printStackTrace();
      return false;
    }
  }
  */

  private List<Integer> getCycRevisionNumbers() {
    return this.cycRevisionNumbers;
  }
}
  private boolean isExecutableAPICompatible()
      throws CycConnectionException, CycApiException, SessionCommunicationException,
          SessionCommandException {

    /*
    if (checkForMissingCriticalFunctions() > 0) {
      return false;
    }
    */
    /*
    // We currently have no way of knowing if this implementation will be compatible with systems
    // *later* than Cyc 10, but it doesn't seem likely. - nwinant, 2015-05-29
    final int serverMajorRevisionNumber = getCycMajorRevisionNumber();
    if (getCycMajorRevisionNumber() != REQUIRED_MAJOR_REVISION) {
      LOGGER.error("This API implementation requires servers at System {}, but {} is at System {}.", REQUIRED_MAJOR_REVISION, server, serverMajorRevisionNumber);
      return false;
    }
    */
    return BASELINE_API_REQUIREMENTS.isCompatible((CycClient) getCyc());

    // TODO: clean up this cruft - nwinant, 2015-06-25

    /*
    if (!BASELINE_API_REQUIREMENTS.isCompatible(getCyc().getCycSession())) {
      return false;
    }*/

    /*
    final int serverMinorRevisionNumber = getCycMinorRevisionNumber();
    final CycServerReleaseType type = getSystemReleaseType();
    final Integer minimumMinorRevisionNumber;
    final String minimumReleaseNumber;
    final String downloadUrl;
    switch (type) {
      case MAINT:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_MAINT;
        minimumReleaseNumber = null;
        downloadUrl = null;
        break;

      case RESEARCHCYC:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_RCYC;
        minimumReleaseNumber = MINIMUM_RCYC_RELEASE;
        downloadUrl = CYC_DOWNLOAD_URL;
        break;

      case ENTERPRISECYC:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_ECYC;
        minimumReleaseNumber = MINIMUM_ECYC_RELEASE;
        downloadUrl = CYC_DOWNLOAD_URL;
        break;

      case OPENCYC:
        minimumMinorRevisionNumber = MINIMUM_MINOR_REVISION_OCYC;
        minimumReleaseNumber = MINIMUM_OCYC_RELEASE;
        downloadUrl = CYC_DOWNLOAD_URL;
        break;

      default:
        LOGGER.error("Unknown system type '{}' for {}, cannot determine compatibility.", getSystemReleaseTypeString(), server);
        return false;
    }

    if (serverMinorRevisionNumber < minimumMinorRevisionNumber) {
      LOGGER.error("{} servers must be at least System {}.{} for API compatibility, but {} is only at System {}.{}.",
              type.getName(),
              serverMajorRevisionNumber, minimumMinorRevisionNumber,
              server,
              serverMajorRevisionNumber, serverMinorRevisionNumber);
      if (minimumReleaseNumber != null) {
        LOGGER.error("The earliest release of {} supported by this API implementation is {} {}.",
                type.getName(),
                type.getName(),
                minimumReleaseNumber);
      }
      if (downloadUrl != null) {
        LOGGER.error("To upgrade your {} server, visit {}.", type.getName(), downloadUrl);
      }
      return false;
    }
    return true;
    */
  }