/**
 * 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.hadoop.gateway.filter.rewrite.impl.json;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.apache.hadoop.gateway.filter.rewrite.api.UrlRewriteFilterApplyDescriptor;
import org.apache.hadoop.gateway.filter.rewrite.api.UrlRewriteFilterBufferDescriptor;
import org.apache.hadoop.gateway.filter.rewrite.api.UrlRewriteFilterContentDescriptor;
import org.apache.hadoop.gateway.filter.rewrite.api.UrlRewriteFilterDetectDescriptor;
import org.apache.hadoop.gateway.filter.rewrite.api.UrlRewriteFilterGroupDescriptor;
import org.apache.hadoop.gateway.filter.rewrite.api.UrlRewriteFilterPathDescriptor;
import org.apache.hadoop.gateway.filter.rewrite.i18n.UrlRewriteMessages;
import org.apache.hadoop.gateway.i18n.messages.MessagesFactory;
import org.apache.hadoop.gateway.util.JsonPath;

import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

class JsonFilterReader extends Reader {

  private static final UrlRewriteMessages LOG = MessagesFactory.get( UrlRewriteMessages.class );

  private static final UrlRewriteFilterPathDescriptor.Compiler<JsonPath.Expression> JPATH_COMPILER = new JsonPathCompiler();
  private static final UrlRewriteFilterPathDescriptor.Compiler<Pattern> REGEX_COMPILER = new RegexCompiler();

  private JsonFactory factory;
  private JsonParser parser;
  private JsonGenerator generator;
  private ObjectMapper mapper;

  private Reader reader;
  private int offset;
  private StringWriter writer;
  private StringBuffer buffer;
  private Stack<Level> stack;
  private Level bufferingLevel;
  private UrlRewriteFilterBufferDescriptor bufferingConfig;
  private UrlRewriteFilterGroupDescriptor config;


  public JsonFilterReader( Reader reader, UrlRewriteFilterContentDescriptor config ) throws IOException {
    this.reader = reader;
    factory = new JsonFactory();
    mapper = new ObjectMapper();
    parser = factory.createParser( reader );
    writer = new StringWriter();
    buffer = writer.getBuffer();
    offset = 0;
    generator = factory.createGenerator( writer );
    stack = new Stack<Level>();
    bufferingLevel = null;
    bufferingConfig = null;
    this.config = config;
  }

  @Override
  public int read( char[] destBuffer, int destOffset, int destCount ) throws IOException {
    int count = 0;
    int available = buffer.length() - offset;

    if( available == 0 ) {
      JsonToken token = parser.nextToken();
      if( token == null ) {
        count = -1;
      } else {
        processCurrentToken();
        available = buffer.length() - offset;
      }
    }

    if( available > 0 ) {
      count = Math.min( destCount, available );
      buffer.getChars( offset, offset+count, destBuffer, destOffset );
      offset += count;
      if( offset == buffer.length() ) {
        offset = 0;
        buffer.setLength( 0 );
      }
    }

    return count;
  }

  private void processCurrentToken() throws IOException {
    switch( parser.getCurrentToken() ) {
      case START_OBJECT:
        processStartObject();
        break;
      case END_OBJECT:
        processEndObject();
        break;
      case START_ARRAY:
        processStartArray();
        break;
      case END_ARRAY:
        processEndArray();
        break;
      case FIELD_NAME:
        processFieldName(); // Could be the name of an object, array or value.
        break;
      case VALUE_STRING:
        processValueString();
        break;
      case VALUE_NUMBER_INT:
      case VALUE_NUMBER_FLOAT:
        processValueNumber();
        break;
      case VALUE_TRUE:
      case VALUE_FALSE:
        processValueBoolean();
        break;
      case VALUE_NULL:
        processValueNull();
        break;
      case NOT_AVAILABLE:
        // Ignore it.
        break;
    }
    generator.flush();
  }

  private Level pushLevel( String field, JsonNode node, JsonNode scopeNode, UrlRewriteFilterGroupDescriptor scopeConfig ) {
    if( !stack.isEmpty() ) {
      Level top = stack.peek();
      if( scopeNode == null ) {
        scopeNode = top.scopeNode;
        scopeConfig = top.scopeConfig;
      }
    }
    Level level = new Level( field, node, scopeNode, scopeConfig );
    stack.push( level );
    return level;
  }

  private void processStartObject() throws IOException {
    JsonNode node;
    Level child;
    Level parent;
    if( stack.isEmpty() ) {
      node = mapper.createObjectNode();
      child = pushLevel( null, node, node, config );
    } else {
      child = stack.peek();
      if( child.node == null ) {
        child.node = mapper.createObjectNode();
        parent = stack.get( stack.size()-2 );
        switch( parent.node.asToken() ) {
          case START_ARRAY:
            ((ArrayNode)parent.node ).add( child.node );
            break;
          case START_OBJECT:
            ((ObjectNode)parent.node ).put( child.field, child.node );
            break;
          default:
            throw new IllegalStateException();
        }
      } else if( child.isArray() ) {
        parent = child;
        node = mapper.createObjectNode();
        child = pushLevel( null, node, null, null );
        ((ArrayNode)parent.node ).add( child.node );
      } else {
        throw new IllegalStateException();
      }
    }
    if( bufferingLevel == null ) {
      if( !startBuffering( child ) ) {
        generator.writeStartObject();
      }
    }
  }

  private void processEndObject() throws IOException {
    Level child;
    Level parent;
    child = stack.pop();
    if( bufferingLevel == child ) {
      filterBufferedNode( child );
      mapper.writeTree( generator, child.node );
      bufferingLevel = null;
      bufferingConfig = null;
    } else if( bufferingLevel == null ) {
      generator.writeEndObject();
      if( !stack.isEmpty() ) {
        parent = stack.peek();
        switch( parent.node.asToken() ) {
          case START_ARRAY:
            ((ArrayNode)parent.node ).removeAll();
            break;
          case START_OBJECT:
            ((ObjectNode)parent.node ).removeAll();
            break;
          default:
            throw new IllegalStateException();
        }
      }
    }
  }

  private void processStartArray() throws IOException {
    JsonNode node;
    Level child;
    Level parent;
    if( stack.isEmpty() ) {
      node = mapper.createArrayNode();
      child = pushLevel( null, node, node, config );
    } else {
      child = stack.peek();
      if( child.node == null ) {
        child.node = mapper.createArrayNode();
        parent = stack.get( stack.size() - 2 );
        switch( parent.node.asToken() ) {
          case START_ARRAY:
            ((ArrayNode)parent.node ).add( child.node );
            break;
          case START_OBJECT:
            ((ObjectNode)parent.node ).put( child.field, child.node );
            break;
          default:
            throw new IllegalStateException();
        }
      } else if( child.isArray() ) {
        parent = child;
        child = pushLevel( null, mapper.createArrayNode(), null, null );
        ((ArrayNode)parent.node ).add( child.node );
      } else {
        throw new IllegalStateException();
      }
    }
    if( bufferingLevel == null ) {
      if( !startBuffering( child ) ) {
        generator.writeStartArray();
      }
    }
  }

  private void processEndArray() throws IOException {
    Level child;
    Level parent;
    child = stack.pop();
    if( bufferingLevel == child ) {
      filterBufferedNode( child );
      mapper.writeTree( generator, child.node );
      bufferingLevel = null;
      bufferingConfig = null;
    } else if( bufferingLevel == null ) {
      generator.writeEndArray();
      if( !stack.isEmpty() ) {
        parent = stack.peek();
        switch( parent.node.asToken() ) {
          case START_ARRAY:
            ((ArrayNode)parent.node ).removeAll();
            break;
          case START_OBJECT:
            ((ObjectNode)parent.node ).removeAll();
            break;
          default:
            throw new IllegalStateException();
        }
      }
    }
  }

  private void processFieldName() throws IOException {
    Level child = pushLevel( parser.getCurrentName(), null, null, null );
    try {
      child.field = filterFieldName( child.field );
    } catch( Exception e ) {
      LOG.failedToFilterFieldName( child.field, e );
      // Write original name.
    }
    if( bufferingLevel == null ) {
      generator.writeFieldName( child.field );
    }
  }

  private void processValueString() throws IOException {
    Level child;
    Level parent;
    String value = null;
    parent = stack.peek();
    if( parent.isArray() ) {
      ArrayNode array = (ArrayNode)parent.node;
      array.add( parser.getText() );
      if( bufferingLevel == null ) {
        value = filterStreamValue( parent );
        array.set( array.size()-1, new TextNode( value ) );
      } else {
        array.removeAll();
      }
    } else {
      child = stack.pop();
      parent = stack.peek();
      ((ObjectNode)parent.node ).put( child.field, parser.getText() );
      if( bufferingLevel == null ) {
        child.node = parent.node; // Populate the JsonNode of the child for filtering.
        value = filterStreamValue( child );
      }
    }
    if( bufferingLevel == null ) {
      if( parent.node.isArray() ) {
        ((ArrayNode)parent.node).removeAll();
      } else {
        ((ObjectNode)parent.node).removeAll();
      }
      generator.writeString( value );
    }
  }

  private void processValueNumber() throws IOException {
    Level child;
    Level parent;
    parent = stack.peek();
    if( parent.isArray() ) {
      if( bufferingLevel != null ) {
        ArrayNode array = (ArrayNode)parent.node;
        processBufferedArrayValueNumber( array );
      }
    } else {
      child = stack.pop();
      if( bufferingLevel != null ) {
        parent = stack.peek();
        ObjectNode object = (ObjectNode)parent.node;
        processBufferedFieldValueNumber( child, object );
      }
    }
    if( bufferingLevel == null ) {
      processedUnbufferedValueNumber();
    }
  }

  private void processedUnbufferedValueNumber() throws IOException {
    switch( parser.getNumberType() ) {
      case INT:
        generator.writeNumber( parser.getIntValue() );
        break;
      case LONG:
        generator.writeNumber( parser.getLongValue() );
        break;
      case BIG_INTEGER:
        generator.writeNumber( parser.getBigIntegerValue() );
        break;
      case FLOAT:
        generator.writeNumber( parser.getFloatValue() );
        break;
      case DOUBLE:
        generator.writeNumber( parser.getDoubleValue() );
        break;
      case BIG_DECIMAL:
        generator.writeNumber( parser.getDecimalValue() );
        break;
    }
  }

  private void processBufferedFieldValueNumber( Level child, ObjectNode object ) throws IOException {
    //object.put( child.field, parser.getDecimalValue() );
    switch( parser.getNumberType() ) {
      case INT:
        object.put( child.field, parser.getIntValue() );
        break;
      case LONG:
        object.put( child.field, parser.getLongValue() );
        break;
      case BIG_INTEGER:
        object.put( child.field, parser.getDecimalValue() );
        break;
      case FLOAT:
        object.put( child.field, parser.getFloatValue() );
        break;
      case DOUBLE:
        object.put( child.field, parser.getDoubleValue() );
        break;
      case BIG_DECIMAL:
        object.put( child.field, parser.getDecimalValue() );
        break;
    }
  }

  private void processBufferedArrayValueNumber( ArrayNode array ) throws IOException {
    //array.add( parser.getDecimalValue() );
    switch( parser.getNumberType() ) {
      case INT:
        array.add( parser.getIntValue() );
        break;
      case LONG:
        array.add( parser.getLongValue() );
        break;
      case BIG_INTEGER:
        array.add( parser.getDecimalValue() );
        break;
      case FLOAT:
        array.add( parser.getFloatValue() );
        break;
      case DOUBLE:
        array.add( parser.getDoubleValue() );
        break;
      case BIG_DECIMAL:
        array.add( parser.getDecimalValue() );
        break;
    }
  }

  private void processValueBoolean() throws IOException {
    Level child;
    Level parent;
    parent = stack.peek();
    if( parent.isArray() ) {
      ((ArrayNode)parent.node ).add( parser.getBooleanValue() );
      //dump();
      if( bufferingLevel == null ) {
        ((ArrayNode)parent.node ).removeAll();
      }
    } else {
      child = stack.pop();
      parent = stack.peek();
      ((ObjectNode)parent.node ).put( child.field, parser.getBooleanValue() );
      //dump();
      if( bufferingLevel == null ) {
        ((ObjectNode)parent.node ).remove( child.field );
      }
    }
    if( bufferingLevel == null ) {
      generator.writeBoolean( parser.getBooleanValue() );
    }
  }

  private void processValueNull() throws IOException {
    Level child;
    Level parent = stack.peek();
    if( parent.isArray() ) {
      ((ArrayNode)parent.node ).addNull();
      //dump();
      if( bufferingLevel == null ) {
        ((ArrayNode)parent.node ).removeAll();
      }
    } else {
      child = stack.pop();
      parent = stack.peek();
      ((ObjectNode)parent.node ).putNull( child.field );
      //dump();
      if( bufferingLevel == null ) {
        ((ObjectNode)parent.node ).remove( child.field );
      }
    }
    if( bufferingLevel == null ) {
      generator.writeNull();
    }
  }

  protected boolean startBuffering( Level node ) {
    boolean buffered = false;
    UrlRewriteFilterGroupDescriptor scope = node.scopeConfig;
    if( scope != null ) {
      for( UrlRewriteFilterPathDescriptor selector : scope.getSelectors() ) {
        JsonPath.Expression path = (JsonPath.Expression)selector.compiledPath( JPATH_COMPILER );
        List<JsonPath.Match> matches = path.evaluate( node.scopeNode );
        if( matches != null && matches.size() > 0 ) {
          if( selector instanceof UrlRewriteFilterBufferDescriptor ) {
            bufferingLevel = node;
            bufferingConfig = (UrlRewriteFilterBufferDescriptor)selector;
            buffered = true;
          }
          break;
        }
      }
    }
    return buffered;
  }

  protected String filterStreamValue( Level node ) {
    String value;
    if( node.isArray() ) {
      value = node.node.get( 0 ).asText();
    } else {
      value = node.node.get( node.field ).asText();
    }
    String rule = null;
    UrlRewriteFilterGroupDescriptor scope = node.scopeConfig;
    //TODO: Scan the top level apply rules for the first match.
    if( scope != null ) {
      for( UrlRewriteFilterPathDescriptor selector : scope.getSelectors() ) {
        JsonPath.Expression path = (JsonPath.Expression)selector.compiledPath( JPATH_COMPILER );
        List<JsonPath.Match> matches = path.evaluate( node.scopeNode );
        if( matches != null && matches.size() > 0 ) {
          JsonPath.Match match = matches.get( 0 );
          if( match.getNode().isTextual() ) {
            if( selector instanceof UrlRewriteFilterApplyDescriptor ) {
              UrlRewriteFilterApplyDescriptor apply = (UrlRewriteFilterApplyDescriptor)selector;
              rule = apply.rule();
              break;
            }
          }
        }
      }
    }
    try {
      value = filterValueString( node.field, value, rule );
      if( node.isArray() ) {
        ((ArrayNode)node.node).set( 0, new TextNode( value ) );
      } else {
        ((ObjectNode)node.node).put( node.field, value );
      }
    } catch( Exception e ) {
      LOG.failedToFilterValue( value, rule, e );
    }
    return value;
  }

  private void filterBufferedNode( Level node ) {
    for( UrlRewriteFilterPathDescriptor selector : bufferingConfig.getSelectors() ) {
      JsonPath.Expression path = (JsonPath.Expression)selector.compiledPath( JPATH_COMPILER );
      List<JsonPath.Match> matches = path.evaluate( node.node );
      for( JsonPath.Match match : matches ) {
        if( selector instanceof UrlRewriteFilterApplyDescriptor ) {
          if( match.getNode().isTextual() ) {
            filterBufferedValue( match, (UrlRewriteFilterApplyDescriptor)selector );
          }
        } else if( selector instanceof UrlRewriteFilterDetectDescriptor ) {
          UrlRewriteFilterDetectDescriptor detectConfig = (UrlRewriteFilterDetectDescriptor)selector;
          JsonPath.Expression detectPath = (JsonPath.Expression)detectConfig.compiledPath( JPATH_COMPILER );
          List<JsonPath.Match> detectMatches = detectPath.evaluate( node.node );
          for( JsonPath.Match detectMatch : detectMatches ) {
            if( detectMatch.getNode().isTextual() ) {
              String detectValue = detectMatch.getNode().asText();
              Pattern detectPattern = detectConfig.compiledValue( REGEX_COMPILER );
              if( detectPattern.matcher( detectValue ).matches() ) {
                filterBufferedValues( node, detectConfig.getSelectors() );
              }
            }
          }
        }
      }
    }
  }

  private void filterBufferedValues( Level node, List<UrlRewriteFilterPathDescriptor> selectors ) {
    for( UrlRewriteFilterPathDescriptor selector : selectors ) {
      JsonPath.Expression path = (JsonPath.Expression)selector.compiledPath( JPATH_COMPILER );
      List<JsonPath.Match> matches = path.evaluate( node.node );
      for( JsonPath.Match match : matches ) {
        if( match.getNode().isTextual() ) {
          if( selector instanceof UrlRewriteFilterApplyDescriptor ) {
            filterBufferedValue( match, (UrlRewriteFilterApplyDescriptor)selector );
          }
        }
      }
    }
  }

  private void filterBufferedValue( JsonPath.Match match, UrlRewriteFilterApplyDescriptor apply ) {
    String field = match.getField();
    String value = match.getNode().asText();
    try {
      value = filterValueString( field, value, apply.rule() );
      ((ObjectNode)match.getParent().getNode()).put( field, value );
    } catch( Exception e ) {
      LOG.failedToFilterValue( value, apply.rule(), e );
    }
  }

  protected String filterFieldName( String field ) {
    return field;
  }

  protected String filterValueString( String name, String value, String rule ) {
    return value;
  }

  @Override
  public void close() throws IOException {
    generator.close();
    writer.close();
    parser.close();
    reader.close();
  }

  private static class Level {
    String field;
    JsonNode node;
    JsonNode scopeNode;
    UrlRewriteFilterGroupDescriptor scopeConfig;
    private Level( String field, JsonNode node, JsonNode scopeNode, UrlRewriteFilterGroupDescriptor scopeConfig ) {
      this.field = field;
      this.node = node;
      this.scopeNode = scopeNode;
      this.scopeConfig = scopeConfig;
    }
    public boolean isArray() {
      return node != null && node.isArray();
    }
  }

  private static class JsonPathCompiler implements UrlRewriteFilterPathDescriptor.Compiler<JsonPath.Expression> {
    @Override
    public JsonPath.Expression compile( String expression, JsonPath.Expression compiled ) {
      return JsonPath.compile( expression );
    }
  }

  private static class RegexCompiler implements UrlRewriteFilterPathDescriptor.Compiler<Pattern> {
    @Override
    public Pattern compile( String expression, Pattern compiled ) {
      if( compiled != null ) {
        return compiled;
      } else {
        return Pattern.compile( expression );
      }
    }
  }

//  private void dump() throws IOException {
//    mapper.writeTree( factory.createGenerator( System.out ), stack.get( 0 ).node );
//    System.out.println();
//  }

}

