/*
 * 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.knox.gateway.util.urltemplate;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;

abstract class Segment {
  static final String ANONYMOUS_PARAM = "";
  static final String DEFAULT_PATTERN = "";
  static final String STAR_PATTERN = "*";
  static final String GLOB_PATTERN = "**";

  static final char DOT = '.';
  static final char ESC = '\\';

  // Note: The order of these is important.  The numbers must be from most specific to least.
  public static final int STATIC = 1;
  public static final int REGEX = 2;
  public static final int STAR = 3;
  public static final int DEFAULT = 4;
  public static final int GLOB = 5;
  public static final int UNKNOWN = 6;

  private Token token;
  private Map<String,Value> values;

  protected Segment( Token token ) {
    this.token = token;
    this.values = new LinkedHashMap<>();
    this.values.put( token.effectivePattern, new Value( token ) );
  }

  protected Token getToken() {
    return token;
  }

  @Override
  public int hashCode() {
    return token.parameterName.hashCode();
  }

  @Override
  @SuppressWarnings( "unchecked" )
  public boolean equals( Object obj ) {
    boolean equal = false;
    if( obj instanceof Segment ) {
      Segment that = (Segment)obj;
      equal = ( this.token.parameterName.equals( that.token.parameterName ) && this.values.size() == that.values.size() );
      if( equal ) {
        for( String pattern: this.values.keySet() ) {
          equal = that.values.containsKey( pattern );
          if( !equal ) {
            break;
          }
        }
      }
    }
    return equal;
  }

  public String getParamName() {
    return token.parameterName;
  }

  public Collection<Value> getValues() {
    return values.values();
  }

  public Value getFirstValue() {
    Value first = null;
    if( !values.isEmpty() ) {
      first = values.values().iterator().next();
    }
    return first;
  }

  public boolean matches( Segment that ) {
    if( getClass().isInstance( that ) ) {
      for( Value thisValue: this.values.values() ) {
        for( Value thatValue: that.values.values() ) {
          if( thisValue.matches( thatValue ) ) {
            return true;
          }
        }
      }
    }
    return false;
  }

  void addValue( Token token ) {
    Value value = new Value( token );
    values.put( token.effectivePattern, value );
  }

  @Override
  public String toString() {
    StringBuilder s = new StringBuilder();
    s.append( getParamName() );
    Collection<Value> values = getValues();
    if( values == null ) {
      s.append( "null" );
    } else if( values.isEmpty() ) {
      s.append( "empty" );
    } else {
      s.append('[');
      Iterator i = values.iterator();
      while( i.hasNext() ) {
        s.append( i.next() );
        if( i.hasNext() ) {
          s.append(',');
        }
      }
      s.append(']');
    }
    return s.toString();
  }

  public class Value {
    private int type;
    private Token token;
    private Pattern regex;

    Value( Token token ) {
      this.token = token;
      this.regex = null;
      String effectivePattern = token.effectivePattern;
      if( token.isLiteral() ) {
        this.type = STATIC;
      } else if( DEFAULT_PATTERN.equals( effectivePattern ) ) {
        this.type = DEFAULT;
      } else if( STAR_PATTERN.equals( effectivePattern ) ) {
        this.type = STAR;
      } else if( GLOB_PATTERN.equals( effectivePattern ) ) {
        type = GLOB;
      } else if ( effectivePattern != null && effectivePattern.contains( STAR_PATTERN ) ) {
        this.type = REGEX;
        this.regex = compileRegex( effectivePattern );
      } else {
        this.type = STATIC;
      }
    }

    Token getToken() {
      return token;
    }

    public int getType() {
      return type;
    }

    public String getPattern() {
      return token.originalPattern;
    }

    String getOriginalPattern() {
      return token.originalPattern;
    }

    String getEffectivePattern() {
      return token.effectivePattern;
    }

    public Pattern getRegex() {
      return regex;
    }

    @Override
    public String toString() {
      return token.effectivePattern;
    }

    public boolean matches( Value that ) {
      boolean matches = getClass().isInstance( that );
      if( matches ) {
        switch( this.getType() ) {
          case( STATIC ):
            matches = this.token.originalPattern.equals( that.token.originalPattern );
            //matches = matchThisStatic( that ); // See: MatcherTest.testWildcardCharacterInInputTemplate
            break;
          case( DEFAULT ):
          case( STAR ):
          case( GLOB ):
            matches = true;
            //matches = matchThisWildcard( that ); // See: MatcherTest.testWildcardCharacterInInputTemplate
            break;
          case( REGEX ):
            matches = this.regex.matcher( that.token.effectivePattern ).matches();
            //matches = matchThisRegex( that ); // See: MatcherTest.testWildcardCharacterInInputTemplate
            break;
          default:
            matches = false;
        }
      }
      return matches;
    }

// See: MatcherTest.testWildcardCharacterInInputTemplate
//    private boolean matchThisStatic( Value that ) {
//      boolean matches = false;
//      switch( that.getType() ) {
//        case( STATIC ):
//          matches = this.pattern.equals( that.pattern );
//          break;
//        case( DEFAULT ):
//        case( STAR ):
//        case( GLOB ):
//          matches = true;
//          break;
//        case( REGEX ):
//          matches = that.regex.matcher( this.pattern ).matches();
//          break;
//      }
//      return matches;
//    }

// See: MatcherTest.testWildcardCharacterInInputTemplate
//    private boolean matchThisWildcard( Value that ) {
//      boolean matches = false;
//      switch( that.getType() ) {
//        case( STATIC ):
//          matches = true;
//          break;
//        case( DEFAULT ):
//        case( STAR ):
//        case( GLOB ):
//          matches = true;
//          break;
//        case( REGEX ):
//          matches = true;
//          break;
//      }
//      return matches;
//    }

// See: MatcherTest.testWildcardCharacterInInputTemplate
//    private boolean matchThisRegex( Value that ) {
//      boolean matches = false;
//      switch( that.getType() ) {
//        case( STATIC ):
//          matches = this.regex.matcher( that.pattern ).matches();
//          break;
//        case( DEFAULT ):
//        case( STAR ):
//        case( GLOB ):
//          matches = true;
//          break;
//        case( REGEX ):
//          matches = this.pattern.equals( that.pattern );
//          break;
//      }
//      return matches;
//    }

  }

  // Escape .\${ and turn * into .*
  static String createRegex(final String segment ) {
    StringBuilder regex = new StringBuilder( segment );
    int len = regex.length();
    for( int i=len-1; i>=0; i-- ) {
      char c = regex.charAt( i );
      switch( c ) {
        case '*':
          regex.insert( i, DOT );
          break;
        case '\\':
        case '.':
        case '{':
        case '}':
        case '$':
          regex.insert( i, ESC );
          break;
        default:
          // noop
      }
    }
    return regex.toString();
  }

  // Creates a pattern for a simplified filesystem style wildcard '*' syntax.
  static Pattern compileRegex(String segment ) {
//    // Turn '*' into '/' to keep it safe.
//    // Chose '/' because that can't exist in a segment.
//    segment = segment.replaceAll( "\\*", "/" );
//    // Turn '.' into '\.'.
//    segment = segment.replaceAll( "\\.", "\\\\." );
//    // Turn '$' into '\$'.
//    segment = escapeSpecialRegExChar( segment, '$' );
//    segment = escapeSpecialRegExChar( segment, '{' );
//    // Turn '/' back into '.*'.
//    segment = segment.replaceAll( "/", "\\.\\*" );
    segment = createRegex( segment );
    return Pattern.compile( segment );
  }
}
