/*
 * $Id$
 *
 * Copyright 2003,2004 The Apache Software Foundation.
 *
 * Licensed 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 com.opensymphony.xwork2.config.impl;

import com.opensymphony.xwork2.util.PatternMatcher;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

import java.io.Serializable;
import java.util.*;

import org.apache.commons.lang.math.NumberUtils;

/**
 * <p> Matches patterns against pre-compiled wildcard expressions pulled from
 * target objects. It uses the wildcard matcher from the Apache Cocoon
 * project. Patterns will be matched in the order they were added. The first 
 * match wins, so more specific patterns should be defined before less specific 
 * patterns.
 * 
 * @since 2.1
 */
public abstract class AbstractMatcher<E> implements Serializable {
    /**
     * <p> The logging instance </p>
     */
    private static final Logger log = LoggerFactory.getLogger(AbstractMatcher.class);

    /**
     * <p> Handles all wildcard pattern matching. </p>
     */
    PatternMatcher<Object> wildcard;

    /**
     * <p> The compiled patterns and their associated target objects </p>
     */
    List<Mapping<E>> compiledPatterns = new ArrayList<Mapping<E>>();;
    
    public AbstractMatcher(PatternMatcher<?> helper) {
        this.wildcard = (PatternMatcher<Object>) helper;
    }

    /**
     * <p>
     * Finds and precompiles the wildcard patterns. Patterns will be evaluated
     * in the order they were added. Only patterns that actually contain a
     * wildcard will be compiled.
     * </p>
     * 
     * <p>
     * Patterns can optionally be matched "loosely". When the end of the pattern
     * matches \*[^*]\*$ (wildcard, no wildcard, wildcard), if the pattern
     * fails, it is also matched as if the last two characters didn't exist. The
     * goal is to support the legacy "*!*" syntax, where the "!*" is optional.
     * </p>
     * 
     * @param name The pattern
     * @param target The object to associate with the pattern
     * @param looseMatch
     *            To loosely match wildcards or not
     */
    public void addPattern(String name, E target, boolean looseMatch) {

        Object pattern;

        if (!wildcard.isLiteral(name)) {
            if (looseMatch && (name.length() > 0) && (name.charAt(0) == '/')) {
                name = name.substring(1);
            }

            if (log.isDebugEnabled()) {
                log.debug("Compiling pattern '" + name + "'");
            }

            pattern = wildcard.compilePattern(name);
            compiledPatterns.add(new Mapping<E>(name, pattern, target));

            if (looseMatch) {
                int lastStar = name.lastIndexOf('*');
                if (lastStar > 1 && lastStar == name.length() - 1) {
                    if (name.charAt(lastStar - 1) != '*') {
                        pattern = wildcard.compilePattern(name.substring(0, lastStar - 1));
                        compiledPatterns.add(new Mapping<E>(name, pattern, target));
                    }
                }
            }
        }
    }
    
    public void freeze() {
        compiledPatterns = Collections.unmodifiableList(new ArrayList<Mapping<E>>());
    }

    /**
     * <p> Matches the path against the compiled wildcard patterns. </p>
     *
     * @param potentialMatch The portion of the request URI for selecting a config.
     * @return The action config if matched, else null
     */
    public E match(String potentialMatch) {
        E config = null;

        if (compiledPatterns.size() > 0) {
            if (log.isDebugEnabled()) {
                log.debug("Attempting to match '" + potentialMatch
                    + "' to a wildcard pattern, "+ compiledPatterns.size()
                    + " available");
            }

            Map<String,String> vars = new LinkedHashMap<String,String>();
            for (Mapping<E> m : compiledPatterns) {
                if (wildcard.match(vars, potentialMatch, m.getPattern())) {
                    if (log.isDebugEnabled()) {
                        log.debug("Value matches pattern '"
                            + m.getOriginalPattern() + "'");
                    }

                    config =
                        convert(potentialMatch, m.getTarget(), vars);
                    break;
                }
            }
        }

        return config;
    }

    /**
     * <p> Clones the target object and its children, replacing various
     * properties with the values of the wildcard-matched strings. </p>
     *
     * @param path The requested path
     * @param orig The original object
     * @param vars A Map of wildcard-matched strings
     * @return A cloned object with appropriate properties replaced with
     *         wildcard-matched values
     */
    protected abstract E convert(String path, E orig, Map<String, String> vars);

    /**
     * <p> Replaces parameter values
     * </p>
     *
     * @param orig  The original parameters with placehold values
     * @param vars  A Map of wildcard-matched strings
     */
    protected Map<String,String> replaceParameters(Map<String, String> orig, Map<String,String> vars) {
        Map<String,String> map = new LinkedHashMap<String,String>();
        
        //this will set the group index references, like {1}
        for (String key : orig.keySet()) {
            map.put(key, convertParam(orig.get(key), vars));
        }
        
        //the values map will contain entries like name->"Lex Luthor" and 1->"Lex Luthor"
        //now add the non-numeric values
        for (String key: vars.keySet()) {
            if (!NumberUtils.isNumber(key)) {
                map.put(key, vars.get(key));
            }
        }
        
        return map;
    }

    /**
     * <p> Inserts into a value wildcard-matched strings where specified
     * with the {x} syntax.  If a wildcard-matched value isn't found, the
     * replacement token is turned into an empty string. 
     * </p>
     *
     * @param val  The value to convert
     * @param vars A Map of wildcard-matched strings
     * @return The new value
     */
    protected String convertParam(String val, Map<String, String> vars) {
        if (val == null) {
            return null;
        } 
        
        int len = val.length();
        StringBuilder ret = new StringBuilder();
        char c;
        String varVal;
        for (int x=0; x<len; x++) {
            c = val.charAt(x);
            if (x < len - 2 && 
                    c == '{' && '}' == val.charAt(x+2)) {
                varVal = (String)vars.get(String.valueOf(val.charAt(x + 1)));
                if (varVal != null) {
                    ret.append(varVal);
                } 
                x += 2;
            } else {
                ret.append(c);
            }
        }
        
        return ret.toString();
    }

    /**
     * <p> Stores a compiled wildcard pattern and the object it came
     * from. </p>
     */
    private static class Mapping<E> implements Serializable {
        /**
         * <p> The original pattern. </p>
         */
        private String original;

        
        /**
         * <p> The compiled pattern. </p>
         */
        private Object pattern;

        /**
         * <p> The original object. </p>
         */
        private E config;

        /**
         * <p> Contructs a read-only Mapping instance. </p>
         *
         * @param original The original pattern
         * @param pattern The compiled pattern
         * @param config  The original object
         */
        public Mapping(String original, Object pattern, E config) {
            this.original = original;
            this.pattern = pattern;
            this.config = config;
        }

        /**
         * <p> Gets the compiled wildcard pattern. </p>
         *
         * @return The compiled pattern
         */
        public Object getPattern() {
            return this.pattern;
        }

        /**
         * <p> Gets the object that contains the pattern. </p>
         *
         * @return The associated object
         */
        public E getTarget() {
            return this.config;
        }
        
        /**
         * <p> Gets the original wildcard pattern. </p>
         *
         * @return The original pattern
         */
        public String getOriginalPattern() {
            return this.original;
        }
    }
}
