/*
 * 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.
 */
package org.apache.lucene.analysis.charfilter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.IntsRefBuilder;
import org.apache.lucene.util.fst.CharSequenceOutputs;
import org.apache.lucene.util.fst.FST;
import org.apache.lucene.util.fst.FSTCompiler;
import org.apache.lucene.util.fst.Outputs;
import org.apache.lucene.util.fst.Util;

// TODO: save/load?

/**
 * Holds a map of String input to String output, to be used with {@link MappingCharFilter}. Use the
 * {@link Builder} to create this.
 */
public class NormalizeCharMap {

  final FST<CharsRef> map;
  final Map<Character, FST.Arc<CharsRef>> cachedRootArcs = new HashMap<>();

  // Use the builder to create:
  private NormalizeCharMap(FST<CharsRef> map) {
    this.map = map;
    if (map != null) {
      try {
        // Pre-cache root arcs:
        final FST.Arc<CharsRef> scratchArc = new FST.Arc<>();
        final FST.BytesReader fstReader = map.getBytesReader();
        map.getFirstArc(scratchArc);
        if (FST.targetHasArcs(scratchArc)) {
          map.readFirstRealTargetArc(scratchArc.target(), scratchArc, fstReader);
          while (true) {
            assert scratchArc.label() != FST.END_LABEL;
            cachedRootArcs.put(
                Character.valueOf((char) scratchArc.label()),
                new FST.Arc<CharsRef>().copyFrom(scratchArc));
            if (scratchArc.isLast()) {
              break;
            }
            map.readNextRealArc(scratchArc, fstReader);
          }
        }
        // System.out.println("cached " + cachedRootArcs.size() + " root arcs");
      } catch (IOException ioe) {
        // Bogus FST IOExceptions!!  (will never happen)
        throw new RuntimeException(ioe);
      }
    }
  }

  /**
   * Builds an NormalizeCharMap.
   *
   * <p>Call add() until you have added all the mappings, then call build() to get a
   * NormalizeCharMap
   *
   * @lucene.experimental
   */
  public static class Builder {

    private final Map<String, String> pendingPairs = new TreeMap<>();

    /**
     * Records a replacement to be applied to the input stream. Whenever <code>singleMatch</code>
     * occurs in the input, it will be replaced with <code>replacement</code>.
     *
     * @param match input String to be replaced
     * @param replacement output String
     * @throws IllegalArgumentException if <code>match</code> is the empty string, or was already
     *     previously added
     */
    public void add(String match, String replacement) {
      if (match.length() == 0) {
        throw new IllegalArgumentException("cannot match the empty string");
      }
      if (pendingPairs.putIfAbsent(match, replacement) != null) {
        throw new IllegalArgumentException("match \"" + match + "\" was already added");
      }
    }

    /** Builds the NormalizeCharMap; call this once you are done calling {@link #add}. */
    public NormalizeCharMap build() {

      final FST<CharsRef> map;
      try {
        final Outputs<CharsRef> outputs = CharSequenceOutputs.getSingleton();
        final FSTCompiler<CharsRef> fstCompiler = new FSTCompiler<>(FST.INPUT_TYPE.BYTE2, outputs);
        final IntsRefBuilder scratch = new IntsRefBuilder();
        for (Map.Entry<String, String> ent : pendingPairs.entrySet()) {
          fstCompiler.add(Util.toUTF16(ent.getKey(), scratch), new CharsRef(ent.getValue()));
        }
        map = fstCompiler.compile();
        pendingPairs.clear();
      } catch (IOException ioe) {
        // Bogus FST IOExceptions!!  (will never happen)
        throw new RuntimeException(ioe);
      }

      return new NormalizeCharMap(map);
    }
  }
}
