package org.apache.qpid.server.exchange.topic;

import org.apache.qpid.framing.AMQShortString;
import org.apache.qpid.framing.AMQShortStringTokenizer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;

/*
*
* 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.
*
*/
public class TopicMatcherDFAState
{
    private static final AtomicInteger stateId = new AtomicInteger();

    private final int _id = stateId.incrementAndGet();

    private final Collection<TopicMatcherResult> _results;
    private final Map<TopicWord, TopicMatcherDFAState> _nextStateMap;
    private static final byte TOPIC_DELIMITTER = (byte)'.';


    public TopicMatcherDFAState(Map<TopicWord, TopicMatcherDFAState> nextStateMap,
                                Collection<TopicMatcherResult> results )
    {
        _nextStateMap = nextStateMap;
        _results = results;
    }


    public TopicMatcherDFAState nextState(TopicWord word)
    {
        final TopicMatcherDFAState nextState = _nextStateMap.get(word);
        return nextState == null ? _nextStateMap.get(TopicWord.ANY_WORD) : nextState;
    }

    public Collection<TopicMatcherResult> terminate()
    {
        return _results;
    }


    public Collection<TopicMatcherResult> parse(TopicWordDictionary dictionary, AMQShortString routingKey)
    {
        return parse(dictionary, routingKey.tokenize(TOPIC_DELIMITTER));
    }

    private Collection<TopicMatcherResult> parse(final TopicWordDictionary dictionary,
                                                 final AMQShortStringTokenizer tokens)
    {
        if(!tokens.hasMoreTokens())
        {
            return _results;
        }
        TopicWord word = dictionary.getWord(tokens.nextToken());
        TopicMatcherDFAState nextState = _nextStateMap.get(word);
        if(nextState == null && word != TopicWord.ANY_WORD)
        {
            nextState = _nextStateMap.get(TopicWord.ANY_WORD);
        }
        if(nextState == null)
        {
            return Collections.EMPTY_LIST;
        }
        // Shortcut if we are at a looping terminal state
        if((nextState == this) && (_nextStateMap.size() == 1) && _nextStateMap.containsKey(TopicWord.ANY_WORD))
        {
            return _results;
        }

        return nextState.parse(dictionary, tokens);
        
    }


    public TopicMatcherDFAState  mergeStateMachines(TopicMatcherDFAState otherStateMachine)
    {
        Map<Set<TopicMatcherDFAState>, TopicMatcherDFAState> newStateMap= new HashMap<Set<TopicMatcherDFAState>, TopicMatcherDFAState>();

        Collection<TopicMatcherResult> results;

        if(_results.isEmpty())
        {
            results = otherStateMachine._results;
        }
        else if(otherStateMachine._results.isEmpty())
        {
            results = _results;
        }
        else
        {
            results = new HashSet<TopicMatcherResult>(_results);
            results.addAll(otherStateMachine._results);
        }


        final Map<TopicWord, TopicMatcherDFAState> newNextStateMap = new HashMap<TopicWord, TopicMatcherDFAState>();

        TopicMatcherDFAState newState = new TopicMatcherDFAState(newNextStateMap, results);


        Set<TopicMatcherDFAState> oldStates = new HashSet<TopicMatcherDFAState>();
        oldStates.add(this);
        oldStates.add(otherStateMachine);

        newStateMap.put(oldStates, newState);

        mergeStateMachines(oldStates, newNextStateMap, newStateMap);

        return newState;

    }

    private static void mergeStateMachines(
            final Set<TopicMatcherDFAState> oldStates,
            final Map<TopicWord, TopicMatcherDFAState> newNextStateMap,
            final Map<Set<TopicMatcherDFAState>, TopicMatcherDFAState> newStateMap)
    {
        Map<TopicWord, Set<TopicMatcherDFAState>> nfaMap = new HashMap<TopicWord, Set<TopicMatcherDFAState>>();

        for(TopicMatcherDFAState state : oldStates)
        {
            Map<TopicWord, TopicMatcherDFAState> map = state._nextStateMap;
            for(Map.Entry<TopicWord, TopicMatcherDFAState> entry : map.entrySet())
            {
                Set<TopicMatcherDFAState> states = nfaMap.get(entry.getKey());
                if(states == null)
                {
                    states = new HashSet<TopicMatcherDFAState>();
                    nfaMap.put(entry.getKey(), states);
                }
                states.add(entry.getValue());
            }
        }

        Set<TopicMatcherDFAState> anyWordStates = nfaMap.get(TopicWord.ANY_WORD);

        for(Map.Entry<TopicWord, Set<TopicMatcherDFAState>> transition : nfaMap.entrySet())
        {
            Set<TopicMatcherDFAState> destinations = transition.getValue();

            if(anyWordStates != null)
            {
                destinations.addAll(anyWordStates);
            }

            TopicMatcherDFAState nextState = newStateMap.get(destinations);
            if(nextState == null)
            {

                if(destinations.size() == 1)
                {
                    nextState = destinations.iterator().next();
                    newStateMap.put(destinations, nextState);
                }
                else
                {
                    Collection<TopicMatcherResult> results;

                    Set<Collection<TopicMatcherResult>> resultSets = new HashSet<Collection<TopicMatcherResult>>();
                    for(TopicMatcherDFAState destination : destinations)
                    {
                        resultSets.add(destination._results);
                    }
                    resultSets.remove(Collections.EMPTY_SET);
                    if(resultSets.size() == 0)
                    {
                        results = Collections.EMPTY_SET;
                    }
                    else if(resultSets.size() == 1)
                    {
                        results = resultSets.iterator().next();
                    }
                    else
                    {
                        results = new HashSet<TopicMatcherResult>();
                        for(Collection<TopicMatcherResult> oldResult : resultSets)
                        {
                            results.addAll(oldResult);
                        }
                    }

                    final Map<TopicWord, TopicMatcherDFAState> nextStateMap = new HashMap<TopicWord, TopicMatcherDFAState>();

                    nextState = new TopicMatcherDFAState(nextStateMap, results);
                    newStateMap.put(destinations, nextState);

                    mergeStateMachines(
                            destinations,
                                       nextStateMap,
                                       newStateMap);


                }


            }
            newNextStateMap.put(transition.getKey(),nextState);
        }

        // Remove redundant transitions where defined tokenWord has same action as ANY_WORD
        TopicMatcherDFAState anyWordState = newNextStateMap.get(TopicWord.ANY_WORD);
        if(anyWordState != null)
        {
            List<TopicWord> removeList = new ArrayList<TopicWord>();
            for(Map.Entry<TopicWord,TopicMatcherDFAState> entry : newNextStateMap.entrySet())
            {
                if(entry.getValue() == anyWordState && entry.getKey() != TopicWord.ANY_WORD)
                {
                    removeList.add(entry.getKey());
                }
            }
            for(TopicWord removeKey : removeList)
            {
                newNextStateMap.remove(removeKey);
            }
        }



    }


    public String toString()
    {
        StringBuilder transitions = new StringBuilder();
        for(Map.Entry<TopicWord, TopicMatcherDFAState> entry : _nextStateMap.entrySet())
        {
            transitions.append("[ ");
            transitions.append(entry.getKey());
            transitions.append("\t ->\t ");
            transitions.append(entry.getValue().getId());
            transitions.append(" ]\n");
        }


        return "[ State " + getId() + " ]\n" + transitions + "\n";

    }

    public String reachableStates()
    {
        StringBuilder result = new StringBuilder("Start state: " + getId() + "\n");

        SortedSet<TopicMatcherDFAState> reachableStates =
                new TreeSet<TopicMatcherDFAState>(new Comparator<TopicMatcherDFAState>()
                                                        {
                                                            public int compare(final TopicMatcherDFAState o1, final TopicMatcherDFAState o2)
                                                            {
                                                                return o1.getId() - o2.getId();
                                                            }
                                                        });
        reachableStates.add(this);

        int count;

        do
        {
            count = reachableStates.size();
            Collection<TopicMatcherDFAState> originalStates = new ArrayList<TopicMatcherDFAState>(reachableStates);
            for(TopicMatcherDFAState state : originalStates)
            {
                reachableStates.addAll(state._nextStateMap.values());
            }
        }
        while(reachableStates.size() != count);



        for(TopicMatcherDFAState state : reachableStates)
        {
            result.append(state.toString());
        }

        return result.toString();
    }


    int getId()
    {
        return _id;
    }
}
