/*
 * 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.exoplatform.services.jcr.impl.core.query.misc;

import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;

import javax.jcr.RepositoryException;

/**
 * Pattern to match normalized {@link QPath}s.
 * A pattern matches either a constant path, a name of a path element, a selection of
 * either of two patterns or a sequence of two patterns. The matching process is greedy.
 * That is, whenever a match is not unique only the longest match is considered.
 * Matching consumes as many elements from the beginning of an input path as possible and
 * returns what's left as an instance of {@link MatchResult}.
 * Use the {@link Matcher} class for matching a whole path or finding matches inside a path.
 */
public abstract class Pattern
{

   /**
    * Matches this pattern against the input.
    * @param input path to match with this pattern
    * @return result from the matching <code>pattern</code> against <code>input</code>
    * @throws IllegalArgumentException if <code>input</code> is not normalized
    */
   public MatchResult match(QPath input)
   {
      try
      {
         return match(new Context(input)).getMatchResult();
      }
      catch (RepositoryException e)
      {
         throw (IllegalArgumentException)new IllegalArgumentException("QPath not normalized").initCause(e);
      }
   }

   protected abstract Context match(Context input) throws RepositoryException;

   /**
    * Construct a new pattern which matches an exact path
    * @param path
    * @return A pattern which matches <code>path</code> and nothing else
    * @throws IllegalArgumentException if <code>path</code> is <code>null</code>
    */
   public static Pattern path(QPath path)
   {
      if (path == null)
      {
         throw new IllegalArgumentException("path cannot be null");
      }
      return new PathPattern(path);
   }

   /**
    * Construct a new pattern which matches a path element of a given name
    * @param name
    * @return A pattern which matches a path element with name <code>name</code>
    * @throws IllegalArgumentException if <code>name</code> is <code>null</code>
    */
   public static Pattern name(QPathEntry name)
   {
      if (name == null)
      {
         throw new IllegalArgumentException("name cannot be null");
      }
      return new NamePattern(name);
   }

   /**
    * Constructs a pattern which matches a path elements against regular expressions.
    * @param namespaceUri A regular expression used for matching the name space URI of
    *   a path element.
    * @param localName A regular expression used for matching the local name of a path
    *   element
    * @return  A pattern which matches a path element if namespaceUri matches the
    *   name space URI of the path element and localName matches the local name of the
    *   path element.
    * @throws IllegalArgumentException if either <code>namespaceUri</code> or
    *   <code>localName</code> is <code>null</code>
    *
    * @see java.util.regex.Pattern
    */
   public static Pattern name(String namespaceUri, String localName)
   {
      if (namespaceUri == null || localName == null)
      {
         throw new IllegalArgumentException("neither namespaceUri nor localName can be null");
      }
      return new RegexPattern(namespaceUri, localName);
   }

   private static final Pattern ALL_PATTERN = new Pattern()
   {
      protected Context match(Context input)
      {
         return input.matchToEnd();
      }

      public String toString()
      {
         return "[ALL]";
      }

   };

   /**
    * A pattern which matches all input.
    * @return
    */
   public static Pattern all()
   {
      return ALL_PATTERN;
   }

   private static final Pattern NOTHING_PATTERN = new Pattern()
   {
      protected Context match(Context input)
      {
         return input.match(0);
      }

      public String toString()
      {
         return "[NOTHING]";
      }
   };

   /**
    * A pattern which matches nothing.
    * @return
    */
   public static Pattern nothing()
   {
      return NOTHING_PATTERN;
   }

   /**
    * A pattern which matches <code>pattern1</code> followed by <code>pattern2</code> and
    * returns the longer of the two matches.
    * @param pattern1
    * @param pattern2
    * @return
    * @throws IllegalArgumentException if either argument is <code>null</code>
    */
   public static Pattern selection(Pattern pattern1, Pattern pattern2)
   {
      if (pattern1 == null || pattern2 == null)
      {
         throw new IllegalArgumentException("Neither pattern can be null");
      }
      return new SelectPattern(pattern1, pattern2);
   }

   /**
    * A pattern which matches <code>pattern1</code> followed by <code>pattern2</code>.
    * @param pattern1
    * @param pattern2
    * @return
    */
   public static Pattern sequence(Pattern pattern1, Pattern pattern2)
   {
      if (pattern1 == null || pattern2 == null)
      {
         throw new IllegalArgumentException("Neither pattern can be null");
      }
      return new SequencePattern(pattern1, pattern2);
   }

   /**
    * A pattern which matches <code>pattern</code> as many times as possible
    * @param pattern
    * @return
    */
   public static Pattern repeat(Pattern pattern)
   {
      if (pattern == null)
      {
         throw new IllegalArgumentException("Pattern can not be null");
      }
      return new RepeatPattern(pattern);
   }

   /**
    * A pattern which matches <code>pattern</code> as many times as possible
    * but at least <code>min</code> times and at most <code>max</code> times.
    * @param pattern
    * @param min
    * @param max
    * @return
    */
   public static Pattern repeat(Pattern pattern, int min, int max)
   {
      if (pattern == null)
      {
         throw new IllegalArgumentException("Pattern can not be null");
      }
      return new RepeatPattern(pattern, min, max);
   }

   // -----------------------------------------------------< Context >---

   private static class Context
   {
      private final QPath path;

      private final int length;

      private final int pos;

      private final boolean isMatch;

      public Context(QPath path)
      {
         super();
         this.path = path;
         length = path.getEntries().length;
         isMatch = false;
         pos = 0;
      }

      public Context(Context context, int pos, boolean matched)
      {
         path = context.path;
         length = context.length;
         this.pos = pos;
         this.isMatch = matched;
         if (pos > length)
         {
            throw new IllegalArgumentException("Cannot match beyond end of input");
         }
      }

      public Context matchToEnd()
      {
         return new Context(this, length, true);
      }

      public Context match(int count)
      {
         return new Context(this, pos + count, true);
      }

      public Context noMatch()
      {
         return new Context(this, this.pos, false);
      }

      public boolean isMatch()
      {
         return isMatch;
      }

      public QPath getRemainder() throws RepositoryException
      {
         if (pos >= length)
         {
            return null;
         }
         else
         {

            //throw new RepositoryException("not implemented");
            return subPath(path, pos, length);
         }
      }

      /**
       * @see Path#subPath(int, int)
       */
      public QPath subPath(QPath source, int from, int to) throws IllegalArgumentException, RepositoryException
      {
         QPathEntry[] elements = source.getEntries();
         if (from < 0 || to > elements.length || from >= to)
         {
            throw new IllegalArgumentException();
         }
         //         if (!isNormalized())
         //         {
         //            throw new RepositoryException("Cannot extract sub-Path from a non-normalized Path: " + this);
         //         }
         QPathEntry[] dest = new QPathEntry[to - from];
         System.arraycopy(elements, from, dest, 0, dest.length);
         //Builder pb = new Builder(dest);
         return new QPath(dest);
      }

      public boolean isExhausted()
      {
         return pos == length;
      }

      public MatchResult getMatchResult()
      {
         return new MatchResult(path, isMatch ? pos : 0);
      }

      public String toString()
      {
         return pos + " @ " + path;
      }

   }

   // -----------------------------------------------------< SelectPattern >---

   private static class SelectPattern extends Pattern
   {
      private final Pattern pattern1;

      private final Pattern pattern2;

      public SelectPattern(Pattern pattern1, Pattern pattern2)
      {
         super();
         this.pattern1 = pattern1;
         this.pattern2 = pattern2;
      }

      protected Context match(Context input) throws RepositoryException
      {
         Context remainder1 = pattern1.match(input);
         Context remainder2 = pattern2.match(input);
         return remainder1.pos > remainder2.pos ? remainder1 : remainder2;
      }

      public String toString()
      {
         return new StringBuffer().append("(").append(pattern1).append("|").append(pattern2).append(")").toString();
      }
   }

   // -----------------------------------------------------< SequencePattern >---

   private static class SequencePattern extends Pattern
   {
      private final Pattern pattern1;

      private final Pattern pattern2;

      public SequencePattern(Pattern pattern1, Pattern pattern2)
      {
         super();
         this.pattern1 = pattern1;
         this.pattern2 = pattern2;
      }

      protected Context match(Context input) throws RepositoryException
      {
         Context context1 = pattern1.match(input);
         if (context1.isMatch())
         {
            return pattern2.match(context1);
         }
         else
         {
            return input.noMatch();
         }
      }

      public String toString()
      {
         return new StringBuffer().append("(").append(pattern1).append(", ").append(pattern2).append(")").toString();
      }
   }

   // -----------------------------------------------------< RepeatPattern >---

   private static class RepeatPattern extends Pattern
   {
      private final Pattern pattern;

      private final int min;

      private final int max;

      private boolean hasBounds;

      public RepeatPattern(Pattern pattern)
      {
         this(pattern, 0, 0);
         this.hasBounds = false;
      }

      public RepeatPattern(Pattern pattern, int min, int max)
      {
         super();
         this.pattern = pattern;
         this.min = min;
         this.max = max;
         this.hasBounds = true;
      }

      protected Context match(Context input) throws RepositoryException
      {
         Context nextInput;
         Context output = input.match(0);
         int matchCount = -1;
         do
         {
            nextInput = output;
            output = pattern.match(nextInput);
            matchCount++;
         }
         while (output.isMatch() && (output.pos > nextInput.pos));

         if (!hasBounds() || (min <= matchCount && matchCount <= max))
         {
            return nextInput;
         }
         else
         {
            return input.noMatch();
         }
      }

      private boolean hasBounds()
      {
         return hasBounds;
      }

      public String toString()
      {
         return new StringBuffer().append("(").append(pattern).append(")*").toString();
      }

   }

   // -----------------------------------------------------< PathPattern >---

   private static class PathPattern extends Pattern
   {
      private final QPath path;

      private final QPathEntry[] patternElements;

      public PathPattern(QPath path)
      {
         super();
         this.path = path;
         patternElements = path.getEntries();
      }

      protected Context match(Context input) throws RepositoryException
      {
         if (input.isExhausted())
         {
            return input;
         }

         QPath inputPath = input.getRemainder();
         //            if (!inputPath.isNormalized()) {
         //                throw new IllegalArgumentException("Not normalized");
         //            }

         QPathEntry[] inputElements = inputPath.getEntries();
         int inputLength = inputElements.length;
         int patternLength = patternElements.length;
         if (patternLength > inputLength)
         {
            return input.noMatch();
         }

         for (int k = 0; k < patternLength; k++)
         {
            if (!patternElements[k].equals(inputElements[k]))
            {
               return input.noMatch();
            }
         }

         return input.match(patternLength);
      }

      public String toString()
      {
         return new StringBuffer().append("\"").append(path).append("\"").toString();
      }
   }

   // -----------------------------------------------------< AbstractNamePattern >---

   private static abstract class AbstractNamePattern extends Pattern
   {
      protected abstract boolean matches(QPathEntry element);

      protected Context match(Context input) throws RepositoryException
      {
         if (input.isExhausted())
         {
            return input.noMatch();
         }

         QPath inputPath = input.getRemainder();
         //            if (!inputPath.isNormalized()) {
         //                throw new IllegalArgumentException("Not normalized");
         //            }

         QPathEntry[] inputElements = inputPath.getEntries();
         if (inputElements.length < 1 || !matches(inputElements[0]))
         {
            return input.noMatch();
         }

         return input.match(1);
      }

   }

   // -----------------------------------------------------< NameNamePattern >---

   private static class NamePattern extends AbstractNamePattern
   {
      private final InternalQName name;

      public NamePattern(InternalQName name)
      {
         super();
         this.name = name;
      }

      protected boolean matches(QPathEntry element)
      {
         return name.equals(element);
      }

      public String toString()
      {
         return new StringBuffer().append("\"").append(name).append("\"").toString();
      }
   }

   // -----------------------------------------------------< StringNamePattern >---

   private static class RegexPattern extends AbstractNamePattern
   {
      private final java.util.regex.Pattern namespaceUri;

      private final java.util.regex.Pattern localName;

      private final String localNameStr;

      private final String namespaceUriStr;

      public RegexPattern(String namespaceUri, String localName)
      {
         super();

         this.namespaceUri = java.util.regex.Pattern.compile(namespaceUri);
         this.localName = java.util.regex.Pattern.compile(localName);
         this.namespaceUriStr = namespaceUri;
         this.localNameStr = localName;
      }

      protected boolean matches(QPathEntry element)
      {
         InternalQName name = element;
         boolean nsMatches = namespaceUri.matcher(name.getNamespace()).matches();
         boolean localMatches = localName.matcher(name.getName()).matches();
         return nsMatches && localMatches;
      }

      public String toString()
      {
         return new StringBuffer().append("\"{").append(namespaceUriStr).append("}").append(localNameStr).append("\"")
            .toString();
      }
   }

}
