package org.apache.helix.filestore;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.I0Itec.zkclient.DataUpdater;
import org.apache.helix.AccessOption;
import org.apache.helix.HelixManager;
import org.apache.helix.NotificationContext;
import org.apache.helix.ZNRecord;
import org.apache.helix.model.InstanceConfig;
import org.apache.helix.model.Message;
import org.apache.helix.participant.statemachine.StateModel;
import org.apache.helix.participant.statemachine.StateModelInfo;
import org.apache.helix.participant.statemachine.Transition;
import org.apache.helix.store.zk.ZkHelixPropertyStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.zookeeper.data.Stat;

@StateModelInfo(initialState = "OFFLINE", states = {
    "OFFLINE", "MASTER", "SLAVE"
})
public class FileStoreStateModel extends StateModel {
  private final class HighWaterMarkUpdater implements DataUpdater<ZNRecord> {
    private final Message message;
    private final ChangeRecord lastRecordProcessed;

    private HighWaterMarkUpdater(Message message, ChangeRecord lastRecordProcessed) {
      this.message = message;
      this.lastRecordProcessed = lastRecordProcessed;
    }

    @Override
    public ZNRecord update(ZNRecord currentData) {
      ZNRecord newRec = new ZNRecord(message.getResourceName());

      if (currentData != null) {
        int currentGen = convertToInt(newRec.getSimpleField("currentGen"), 0);
        int currentGenStartSeq = convertToInt(newRec.getSimpleField("currentGenStartSeq"), 0);
        int prevGen = convertToInt(newRec.getSimpleField("prevGen"), 0);
        int prevGenEndSeq = convertToInt(newRec.getSimpleField("prevGenEndSeq"), 0);
        newRec.setSimpleField("currentGen", Integer.toString(currentGen + 1));
        newRec.setSimpleField("currentGenStartSeq", Integer.toString(1));
        if (currentGen > 0) {
          newRec.setSimpleField("prevGen", Integer.toString(currentGen));
          int localEndSeq = 1;
          if (lastRecordProcessed != null) {
            localEndSeq = (int) lastRecordProcessed.txid;
          }
          newRec.setSimpleField("prevGenEndSeq", "" + localEndSeq);
        }
        newRec.merge(currentData);
      } else {
        newRec.setSimpleField("currentGen", Integer.toString(1));
        newRec.setSimpleField("currentGenStartSeq", Integer.toString(1));
      }
      return newRec;

    }

    private int convertToInt(String number, int defaultValue) {
      try {
        if (number != null) {
          return Integer.parseInt(number);
        }
      } catch (Exception e) {

      }
      return defaultValue;
    }
  }

  private static Logger LOG = LoggerFactory.getLogger(FileStoreStateModel.class);

  private final String _serverId;
  private final String _partition;

  private Replicator replicator;

  private ChangeLogGenerator generator;

  private FileSystemWatchService service;

  private InstanceConfig instanceConfig;

  public FileStoreStateModel(HelixManager manager, String resource, String partition) {
    String clusterName = manager.getClusterName();
    String instanceName = manager.getInstanceName();
    instanceConfig = manager.getClusterManagmentTool().getInstanceConfig(clusterName, instanceName);
    replicator = new Replicator(instanceConfig, resource, partition);
    try {
      manager.addExternalViewChangeListener(replicator);
    } catch (Exception e) {
      e.printStackTrace();
    }
    _partition = partition;
    _serverId = instanceName;
  }

  /**
   * If the node is slave, start the rsync thread if it is not started
   * @param message
   * @param context
   * @throws Exception
   */

  @Transition(from = "OFFLINE", to = "SLAVE")
  public void onBecomeSlaveFromOffline(Message message, NotificationContext context)
      throws Exception {
    System.out.println(_serverId + " transitioning from " + message.getFromState() + " to "
        + message.getToState() + " for " + _partition);

    replicator.start();
    System.out.println(_serverId + " transitioned from " + message.getFromState() + " to "
        + message.getToState() + " for " + _partition);
  }

  /**
   * When the node becomes master, it will start accepting writes and increments
   * the epoch and starts logging the changes in a file
   * @param message
   * @param context
   * @throws Exception
   */
  @Transition(from = "SLAVE", to = "MASTER")
  public void onBecomeMasterFromSlave(final Message message, NotificationContext context)
      throws Exception {
    replicator.stop();
    System.out.println(_serverId + " transitioning from " + message.getFromState() + " to "
        + message.getToState() + " for " + _partition);
    ZkHelixPropertyStore<ZNRecord> helixPropertyStore =
        context.getManager().getHelixPropertyStore();
    String checkpointDirPath = instanceConfig.getRecord().getSimpleField("check_point_dir");
    CheckpointFile checkpointFile = new CheckpointFile(checkpointDirPath);
    final ChangeRecord lastRecordProcessed = checkpointFile.findLastRecordProcessed();
    DataUpdater<ZNRecord> updater = new HighWaterMarkUpdater(message, lastRecordProcessed);
    helixPropertyStore.update("/TRANSACTION_ID_METADATA" + "/" + message.getResourceName(),
        updater, AccessOption.PERSISTENT);
    Stat stat = new Stat();
    ;
    ZNRecord znRecord =
        helixPropertyStore.get("/TRANSACTION_ID_METADATA" + "/" + message.getResourceName(), stat,
            AccessOption.PERSISTENT);
    int startGen = Integer.parseInt(znRecord.getSimpleField("currentGen"));
    int startSeq = Integer.parseInt(znRecord.getSimpleField("currentGenStartSeq"));
    String fileStoreDir = instanceConfig.getRecord().getSimpleField("file_store_dir");
    String changeLogDir = instanceConfig.getRecord().getSimpleField("change_log_dir");

    generator = new ChangeLogGenerator(changeLogDir, startGen, startSeq);
    // To indicate that we need callbacks for changes that happen starting now
    long now = System.currentTimeMillis();
    service = new FileSystemWatchService(fileStoreDir, now, generator);
    service.start();
    System.out.println(_serverId + " transitioned from " + message.getFromState() + " to "
        + message.getToState() + " for " + _partition);
  }

  /**
   * Stop writing
   * @param message
   * @param context
   * @throws Exception
   */

  @Transition(from = "MASTER", to = "SLAVE")
  public void onBecomeSlaveFromMaster(Message message, NotificationContext context)
      throws Exception {
    service.stop();
    LOG.info(_serverId + " transitioning from " + message.getFromState() + " to "
        + message.getToState() + " for " + _partition);
    replicator.start();
  }

  @Transition(from = "SLAVE", to = "OFFLINE")
  public void onBecomeOfflineFromSlave(Message message, NotificationContext context) {
    replicator.stop();
    LOG.info(_serverId + " transitioning from " + message.getFromState() + " to "
        + message.getToState() + " for " + _partition);
  }

  public void onBecomeDroppedFromOffline(Message message, NotificationContext context) {
    LOG.info(_serverId + " Dropping partition " + _partition);
  }

  @Override
  public void reset() {
    LOG.warn("Default reset() invoked");
  }
}
