package morfologik.tools;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.ParametersDelegate;

import morfologik.fsa.FSA;
import morfologik.fsa.builders.FSABuilder;
import morfologik.fsa.builders.FSASerializer;
import morfologik.stemming.BufferUtils;
import morfologik.stemming.Dictionary;
import morfologik.stemming.DictionaryLookup;
import morfologik.stemming.DictionaryMetadata;
import morfologik.stemming.ISequenceEncoder;

/**
 * Decompiles morphological dictionary automaton back to source state. 
 */
@Parameters(
    commandNames = "dict_compile",
    commandDescription = "Compiles a morphological dictionary automaton.")
public class DictCompile extends CliTool {
  @Parameter(
      names = {"-i", "--input"},
      description = "The input file (base,inflected,tag). An associated metadata (*.info) file must exist.", 
      required = true,
      validateValueWith = ValidateFileExists.class)
  private Path input;

  @Parameter(
      names = ARG_VALIDATE,
      arity = 1,
      description = "Validate input to make sure it makes sense.")
  private boolean validate = true;  

  @Parameter(
      names = {"-f", "--format"},
      description = "Automaton serialization format.")
  private SerializationFormat format = SerializationFormat.FSA5;  

  @Parameter(
      names = ARG_OVERWRITE,
      description = "Overwrite the output file if it exists.")
  private boolean overwrite;  

  @ParametersDelegate
  private final BinaryInput binaryInput;

  DictCompile() {
    binaryInput = new BinaryInput();
  }
  
  public DictCompile(Path input,
                     boolean overwrite,
                     boolean validate,
                     boolean acceptBom,
                     boolean acceptCr,
                     boolean ignoreEmpty) {
    this.input = checkNotNull(input);
    this.overwrite = overwrite;
    this.validate = validate;
    this.binaryInput = new BinaryInput(acceptBom, acceptCr, ignoreEmpty);
  }

  @Override
  public ExitStatus call() throws Exception {
    final Path metadataPath = DictionaryMetadata.getExpectedMetadataLocation(input);

    if (!Files.isRegularFile(metadataPath)) {
      System.err.println("Dictionary metadata file for the input does not exist: " + metadataPath);
      System.err.println("The metadata file (with at least the column separator and byte encoding) "
          + "is required. Check out the examples."); 
      return ExitStatus.ERROR_OTHER;
    }

    final Path output = metadataPath.resolveSibling(
        metadataPath.getFileName().toString().replaceAll(
            "\\." + DictionaryMetadata.METADATA_FILE_EXTENSION + "$", ".dict"));

    if (!overwrite && Files.exists(output)) {
      throw new ExitStatusException(ExitStatus.ERROR_CONFIRMATION_REQUIRED, 
          "Output dictionary file already exists: %s, use %s to override.", output, ARG_OVERWRITE);
    }
    
    final DictionaryMetadata metadata;
    try (InputStream is = new BufferedInputStream(Files.newInputStream(metadataPath))) {
      metadata = DictionaryMetadata.read(is);
    }

    final List<byte[]> sequences = binaryInput.readBinarySequences(input, (byte) '\n');

    final CharsetDecoder charsetDecoder = metadata.getDecoder()
        .onMalformedInput(CodingErrorAction.REPORT)
        .onUnmappableCharacter(CodingErrorAction.REPORT);

    final byte separator = metadata.getSeparator();
    final ISequenceEncoder sequenceEncoder = metadata.getSequenceEncoderType().get();

    if (!sequences.isEmpty()) {
      Iterator<byte[]> i = sequences.iterator();
      byte [] row = i.next();
      final int separatorCount = countOf(separator, row);

      if (separatorCount < 1 || separatorCount > 2) {
        throw new ExitStatusException(ExitStatus.ERROR_OTHER, 
            "Invalid input. Each row must consist of [base,inflected,tag?] columns, where ',' is a "
            + "separator character (declared as: %s). This row contains %d separator characters: %s",
              Character.isJavaIdentifierPart(metadata.getSeparatorAsChar()) 
                ? "'" + Character.toString(metadata.getSeparatorAsChar()) + "'"
                : "0x" + Integer.toHexString((int) separator & 0xff),
              separatorCount,
              new String(row, charsetDecoder.charset()));
      }

      while (i.hasNext()) {
        row = i.next();
        int count = countOf(separator, row);
        if (count != separatorCount) {
          throw new ExitStatusException(ExitStatus.ERROR_OTHER,
              "The number of separators (%d) is inconsistent with previous lines: %s",
              count,
              new String(row, charsetDecoder.charset()));
        }
      }
    }

    ByteBuffer encoded = ByteBuffer.allocate(0);
    ByteBuffer source = ByteBuffer.allocate(0);
    ByteBuffer target = ByteBuffer.allocate(0);
    ByteBuffer tag = ByteBuffer.allocate(0);
    ByteBuffer assembled = ByteBuffer.allocate(0);
    for (int i = 0, max = sequences.size(); i < max; i++) {
      byte[] row = sequences.get(i);
      int sep1 = indexOf(separator, row, 0);
      int sep2 = indexOf(separator, row, sep1 + 1);
      if (sep2 < 0) {
        sep2 = row.length;
      }

      source = BufferUtils.clearAndEnsureCapacity(source, sep1);
      source.put(row, 0, sep1);
      source.flip();

      final int len = sep2 - (sep1 + 1);
      target = BufferUtils.clearAndEnsureCapacity(target, len);
      target.put(row, sep1 + 1, len);
      target.flip();

      final int len2 = row.length - (sep2 + 1);
      tag = BufferUtils.clearAndEnsureCapacity(tag, len2);
      if (len2 > 0) {
        tag.put(row, sep2 + 1, len2);
      }
      tag.flip();

      encoded = sequenceEncoder.encode(encoded, target, source);

      assembled = BufferUtils.clearAndEnsureCapacity(assembled, 
          target.remaining() + 1 +
          encoded.remaining() + 1 + 
          tag.remaining());

      assembled.put(target);
      assembled.put(separator);
      assembled.put(encoded);
      if (tag.hasRemaining()) {
        assembled.put(separator);
        assembled.put(tag);
      }
      assembled.flip();

      sequences.set(i, BufferUtils.toArray(assembled));
    }

    Collections.sort(sequences, FSABuilder.LEXICAL_ORDERING);
    FSA fsa = FSABuilder.build(sequences);

    FSASerializer serializer = format.getSerializer();
    try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(output))) {
      serializer.serialize(fsa, os);
    }

    // If validating, try to scan the input
    if (validate) {
      DictionaryLookup dictionaryLookup = new DictionaryLookup(new Dictionary(fsa, metadata));
      for (Iterator<?> i = dictionaryLookup.iterator(); i.hasNext(); i.next()) {
        // Do nothing, just scan and make sure no exceptions are thrown.
      }
    }

    return ExitStatus.SUCCESS;
  }

  private static int countOf(byte separator, byte[] row) {
    int cnt = 0;
    for (int i = row.length; --i >= 0;) {
      if (row[i] == separator) {
        cnt++;
      }
    }
    return cnt;
  }

  private static int indexOf(byte separator, byte[] row, int fromIndex) {
    while (fromIndex < row.length) {
      if (row[fromIndex] == separator) {
        return fromIndex;
      }
      fromIndex++;
    }
    return -1;
  }

  public static void main(String[] args) {
    main(args, new DictCompile());
  }
}
