/* LanguageTool, a natural language style checker
 * Copyright (C) 2011 Daniel Naber (http://www.danielnaber.de)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
 * USA
 */
package org.languagetool.openoffice;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.sun.star.beans.PropertyState;
import com.sun.star.beans.PropertyValue;
import com.sun.star.linguistic2.SingleProofreadingError;

/**
 * Class for storing and handle the LT results prepared to use in LO/OO
 *
 * @author Fred Kruse
 * @since 4.3
 */
class ResultCache implements Serializable {

  private static final long serialVersionUID = 2L;
  private Map<Integer, SerialCacheEntry> entries;
  
  private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
  
  ResultCache() {
    this(null);
  }

  public ResultCache(ResultCache cache) {
    replace(cache);
  }

  /**
   * Get cache entry map
   */
  private Map<Integer, SerialCacheEntry> getMap() {
    rwLock.readLock().lock();
    try {
      return new HashMap<Integer, SerialCacheEntry>(entries);
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * Replace the cache content
   */
  void replace(ResultCache cache) {
    rwLock.writeLock().lock();
    try {
      if (cache == null || cache.entries == null) {
        entries = new HashMap<>();
      } else {
        entries = cache.getMap();
      }
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * Remove all cache entries for a paragraph
   */
  void remove(int numberOfParagraph) {
    rwLock.writeLock().lock();
    try {
      entries.remove(numberOfParagraph);
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * Remove all cache entries between firstParagraph and lastParagraph
   */
  void removeRange(int firstParagraph, int lastParagraph) {
    rwLock.writeLock().lock();
    try {
      for (int i = firstParagraph; i <= lastParagraph; i++) {
        entries.remove(i);
      }
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * Remove all cache entries between firstPara (included) and lastPara (excluded)
   * shift all numberOfParagraph by 'shift'
   */
  void removeAndShift(int firstParagraph, int lastParagraph, int oldSize, int newSize) {
    int shift = newSize - oldSize;
    if (lastParagraph < firstParagraph || shift == 0) {
      return;
    }
    rwLock.writeLock().lock();
    try {
      Map<Integer, SerialCacheEntry> tmpEntries = entries;
      entries = new HashMap<>();
      if (shift < 0) {
        for (int i : tmpEntries.keySet()) {
          if (i >= firstParagraph - shift) {
            entries.put(i + shift, tmpEntries.get(i));
          } else if (i < firstParagraph){
            entries.put(i, tmpEntries.get(i));
          } 
        }
      } else {
        for (int i : tmpEntries.keySet()) {
          if (i >= lastParagraph && i + shift >= 0) {
            entries.put(i + shift, tmpEntries.get(i));
          } else if (i < firstParagraph) {
            entries.put(i, tmpEntries.get(i));
          } 
        }
      }
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * add or replace a cache entry
   */
  void put(int numberOfParagraph, List<Integer> nextSentencePositions, SingleProofreadingError[] errorArray) {
    rwLock.writeLock().lock();
    try {
      entries.put(numberOfParagraph, new SerialCacheEntry(nextSentencePositions, errorArray));
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * add or replace a cache entry for paragraph
   */
  void put(int numberOfParagraph, SingleProofreadingError[] errorArray) {
    rwLock.writeLock().lock();
    try {
      entries.put(numberOfParagraph, new SerialCacheEntry(null, errorArray));
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * add proof reading errors to a cache entry for paragraph
   */
  void add(int numberOfParagraph, SingleProofreadingError[] errorArray) {
    rwLock.writeLock().lock();
    try {
      SerialCacheEntry cacheEntry = entries.get(numberOfParagraph);
      cacheEntry.addErrorArray(errorArray);
      entries.put(numberOfParagraph, cacheEntry);
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * Remove all cache entries
   */
  void removeAll() {
    rwLock.writeLock().lock();
    try {
      entries.clear();
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * get cache entry of paragraph
   */
  CacheEntry getCacheEntry(int numberOfParagraph) {
    rwLock.readLock().lock();
    try {
      SerialCacheEntry entry = entries.get(numberOfParagraph);
      return entry == null ? null : new CacheEntry(entry);
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get cache entry of paragraph
   */
  SerialCacheEntry getSerialCacheEntry(int numberOfParagraph) {
    rwLock.readLock().lock();
    try {
      return entries.get(numberOfParagraph);
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get Proofreading errors of on paragraph from cache
   */
  SingleProofreadingError[] getMatches(int numberOfParagraph) {
    rwLock.readLock().lock();
    try {
      SerialCacheEntry entry = entries.get(numberOfParagraph);
      if (entry == null) {
        return null;
      }
      return entry.getErrorArray();
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get start sentence position from cache
   */
  int getStartSentencePosition(int numberOfParagraph, int sentencePosition) {
    rwLock.readLock().lock();
    try {
      SerialCacheEntry entry = entries.get(numberOfParagraph);
      if (entry == null) {
        return 0;
      }
      List<Integer> nextSentencePositions = entry.nextSentencePositions;
      if (nextSentencePositions == null || nextSentencePositions.size() < 2) {
        return 0;
      }
      int startPosition = 0;
      for (int position : nextSentencePositions) {
        if (position >= sentencePosition) {
          return position == sentencePosition ? position : startPosition;
        }
        startPosition = position;
      }
      return nextSentencePositions.get(nextSentencePositions.size() - 2);
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get next sentence position from cache
   */
  int getNextSentencePosition(int numberOfParagraph, int sentencePosition) {
    rwLock.readLock().lock();
    try {
      SerialCacheEntry entry = entries.get(numberOfParagraph);
      if (entry == null) {
        return 0;
      }
      List<Integer> nextSentencePositions = entry.nextSentencePositions;
      if (nextSentencePositions == null || nextSentencePositions.size() == 0) {
        return 0;
      }
      for (int position : nextSentencePositions) {
        if (position > sentencePosition) {
          return position;
        }
      }
      return nextSentencePositions.get(nextSentencePositions.size() - 1);
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get Proofreading errors of sentence out of paragraph matches from cache
   */
  SingleProofreadingError[] getFromPara(int numberOfParagraph,
                                        int startOfSentencePosition, int endOfSentencePosition) {
    rwLock.readLock().lock();
    try {
      SerialCacheEntry entry = entries.get(numberOfParagraph);
      if (entry == null) {
        return null;
      }
      List<SingleProofreadingError> errorList = new ArrayList<>();
      for (SingleProofreadingError eArray : entry.getErrorArray()) {
        if (eArray.nErrorStart >= startOfSentencePosition && eArray.nErrorStart < endOfSentencePosition) {
          errorList.add(eArray);
        }
      }
      return errorList.toArray(new SingleProofreadingError[0]);
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * Compares to Entries
   * true if the both entries are identically
   */
  static boolean areDifferentEntries(SerialCacheEntry newEntries, SerialCacheEntry oldEntries) {
    if (newEntries == null || oldEntries == null) {
      return true;
    }
    SingleProofreadingError[] oldErrorArray = oldEntries.getErrorArray();
    SingleProofreadingError[] newErrorArray = newEntries.getErrorArray();
    if (oldErrorArray == null || newErrorArray == null || oldErrorArray.length != newErrorArray.length) {
      return true;
    }
    for (SingleProofreadingError nError : newErrorArray) {
      boolean found = false;
      for (SingleProofreadingError oError : oldErrorArray) {
        if (nError.nErrorStart == oError.nErrorStart && nError.nErrorLength == oError.nErrorLength
                && nError.aRuleIdentifier.equals(oError.aRuleIdentifier)) {
          found = true;
          break;
        }
      }
      if (!found) {
        return true;
      }
    }
    return false;
  }

  /**
   * true if entry has no error
   */
  static boolean isEmptyEntry(SerialCacheEntry entry) {
    if (entry == null || entry.errorArray == null || entry.errorArray.length == 0) {
      return true;
    }
    return false;
  }

  /**
   * Compares a paragraph cache with another cache.
   * Gives back a list of entries for every paragraph: true if the both entries are identically
   */
  List<Integer> differenceInCaches(ResultCache oldCache) {
    rwLock.readLock().lock();
    try {
      List<Integer> differentParas = new ArrayList<>();
      SerialCacheEntry oEntry;
      SerialCacheEntry nEntry;
      boolean isDifferent = true;
      Set<Integer> entrySet = new HashSet<>(entries.keySet());
      for (int nPara : entrySet) {
        if (oldCache != null) {
          nEntry = entries.get(nPara);
          oEntry = oldCache.entries.get(nPara);
          isDifferent = areDifferentEntries(nEntry, oEntry);
        }
        if (isDifferent) {
          differentParas.add(nPara);
        }
      }
      return differentParas;
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * Remove a special Proofreading error from cache
   * Returns all changed paragraphs as list
   */
  List<Integer> removeRuleError(String ruleId) {
    rwLock.writeLock().lock();
    try {
      List<Integer> changed = new ArrayList<>();
      Set<Integer> keySet = entries.keySet();
      for (int n : keySet) {
        SerialCacheEntry entry = entries.get(n);
        SingleProofreadingError[] eArray = entry.getErrorArray();
        int nErr = 0;
        for (SingleProofreadingError sError : eArray) {
          if (sError.aRuleIdentifier.equals(ruleId)) {
            nErr++;
          }
        }
        if (nErr > 0) {
          changed.add(n);
          SingleProofreadingError[] newArray = new SingleProofreadingError[eArray.length - nErr];
          for (int i = 0, j = 0; i < eArray.length && j < newArray.length; i++) {
            if (!eArray[i].aRuleIdentifier.equals(ruleId)) {
              newArray[j] = eArray[i];
              j++;
            }
          }
          entries.put(n, new SerialCacheEntry(entry.nextSentencePositions, newArray));
        }
      }
      return changed;
    } finally {
      rwLock.writeLock().unlock();
    }
  }

  /**
   * get number of paragraphs stored in cache
   */
  int getNumberOfParas() {
    rwLock.readLock().lock();
    try {
      return entries.size();
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get number of entries
   */
  int getNumberOfEntries() {
    rwLock.readLock().lock();
    try {
      return entries.size();
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get number of matches
   */
  int getNumberOfMatches() {
    rwLock.readLock().lock();
    try {
      int number = 0;
      for (int n : entries.keySet()) {
        number += entries.get(n).errorArray.length;
      }
      return number;
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * get an error from a position within a paragraph
   * if there are more than one error at the position return the one which begins at second
   * if there are more than one that begins at the same position return the one with the smallest size
   */
  SingleProofreadingError getErrorAtPosition(int numPara, int numChar) {
    rwLock.readLock().lock();
    try {
      SerialCacheEntry entry = entries.get(numPara);
      if (entry == null) {
        return null;
      }
      SingleProofreadingError error = null;
      for (SingleProofreadingError err : entry.getErrorArray()) {
        if (numChar >= err.nErrorStart && numChar <= err.nErrorStart + err.nErrorLength) {
          if (error == null || error.nErrorStart < err.nErrorStart
              || (error.nErrorStart == err.nErrorStart && error.nErrorLength > err.nErrorLength)) {
            error = err;
          } 
        }
      }
      return error;
    } finally {
      rwLock.readLock().unlock();
    }
  }

  /**
   * Class of serializable cache entries
   */
  public static class CacheEntry {
    SingleProofreadingError[] errorArray;
    List<Integer> nextSentencePositions = null;

    CacheEntry(SerialCacheEntry entry) {
      if (entry.nextSentencePositions != null) {
        this.nextSentencePositions = new ArrayList<Integer>(entry.nextSentencePositions);
      }
      this.errorArray = new SingleProofreadingError[entry.errorArray.length];
      for (int i = 0; i < entry.errorArray.length; i++) {
        this.errorArray[i] = entry.errorArray[i].toSingleProofreadingError();
      }
    }

    /**
     * Get an SingleProofreadingError array for one entry
     */
    SingleProofreadingError[] getErrorArray() {
      return errorArray;
    }
  }
    
  /**
   * Class of serializable cache entries
   */
  private class SerialCacheEntry implements Serializable {
    private static final long serialVersionUID = 2L;
    SerialProofreadingError[] errorArray;
    List<Integer> nextSentencePositions = null;

    SerialCacheEntry(List<Integer> nextSentencePositions, SingleProofreadingError[] sErrorArray) {
      if (nextSentencePositions != null) {
        this.nextSentencePositions = new ArrayList<Integer>(nextSentencePositions);
      }
      this.errorArray = new SerialProofreadingError[sErrorArray.length];
      for (int i = 0; i < sErrorArray.length; i++) {
        this.errorArray[i] = new SerialProofreadingError(sErrorArray[i]);
      }
    }
    
    /**
     * Get an SingleProofreadingError array for one entry
     */
    SingleProofreadingError[] getErrorArray() {
      SingleProofreadingError[] eArray = new SingleProofreadingError[errorArray.length];
      for (int i = 0; i < errorArray.length; i++) {
        eArray[i] = errorArray[i].toSingleProofreadingError();
      }
      return eArray;
    }
    
    /**
     * Add an SingleProofreadingError array to an existing one
     */
    void addErrorArray(SingleProofreadingError[] errors) {
      if (errors == null || errors.length == 0) {
        return;
      }
      SerialProofreadingError newErrorArray[] = new SerialProofreadingError[errorArray.length + errors.length];
      for (int i = 0; i < errorArray.length; i++) {
        newErrorArray[i] = errorArray[i];
      }
      for (int i = 0; i < errors.length; i++) {
        newErrorArray[errorArray.length + i] = new SerialProofreadingError(errors[i]);
      }
    }
  }
  
  /**
   * Class of serializable proofreading errors
   */
  private class SerialProofreadingError implements Serializable {

    private static final long serialVersionUID = 1L;
    int nErrorStart;
    int nErrorLength;
    int nErrorType;
    String aFullComment;
    String aRuleIdentifier;
    String aShortComment;
    String[] aSuggestions;
    SerialPropertyValue[] aProperties = null;
    
    SerialProofreadingError(SingleProofreadingError error) {
      nErrorStart = error.nErrorStart;
      nErrorLength = error.nErrorLength;
      nErrorType = error.nErrorType;
      aFullComment = error.aFullComment;
      aRuleIdentifier = error.aRuleIdentifier;
      aShortComment = error.aShortComment;
      aSuggestions = error.aSuggestions;
      if (error.aProperties != null) {
        aProperties = new SerialPropertyValue[error.aProperties.length];
        for (int i = 0; i < error.aProperties.length; i++) {
          aProperties[i] = new SerialPropertyValue(error.aProperties[i]);
        }
      }
    }
    
    SingleProofreadingError toSingleProofreadingError () {
      SingleProofreadingError error = new SingleProofreadingError();
      error.nErrorStart = nErrorStart;
      error.nErrorLength = nErrorLength;
      error.nErrorType = nErrorType;
      error.aFullComment = aFullComment;
      error.aRuleIdentifier = aRuleIdentifier;
      error.aShortComment = aShortComment;
      error.aSuggestions = aSuggestions;
      if (aProperties != null) {
        error.aProperties = new PropertyValue[aProperties.length];
        for (int i = 0; i < aProperties.length; i++) {
          error.aProperties[i] = aProperties[i].toPropertyValue();
        }
      } else {
        error.aProperties = null;
      }
      return error;
    }
  }
  
  /**
   * Class of serializable property values
   */
  private class SerialPropertyValue implements Serializable {

    private static final long serialVersionUID = 1L;
    String name;
    Object value;
    
    SerialPropertyValue(PropertyValue properties) {
      name = properties.Name;
      value = properties.Value;
    }
    
    PropertyValue toPropertyValue() {
      PropertyValue properties = new PropertyValue();
      properties.Name = name;
      properties.Value = value;
      properties.Handle = -1;
      properties.State = PropertyState.DIRECT_VALUE;
      return properties;
    }
  }

}
