/*
 * 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.jena.atlas.web;

import java.util.* ;

import org.apache.jena.atlas.logging.Log ;

public class AcceptList
{
    private List<MediaRange> ranges ;
    /** 
     * Create an empty list of accept items from the give strings.
     * @param acceptStrings
     */
    
    private AcceptList()
    { ranges = new ArrayList<>() ; }

    /**
     * Create a list of accept items from the give strings.
     * @param mediaRanges
     */
    
    public AcceptList(List<MediaRange> mediaRanges)
    { ranges = new ArrayList<>(mediaRanges) ; }
    
    /**
     * Create a list of accept items from the give MediaTypes.
     * @param acceptItems
     */
    
    public AcceptList(MediaRange...acceptItems)
    { ranges = Arrays.asList(acceptItems) ; }
    
    /**
     * Create a list of accept items from the give MediaTypes.
     * @param acceptItems
     */
    
    public static AcceptList create(MediaType... acceptItems) {
        AcceptList accepList = new AcceptList();
        for ( MediaType mtype : acceptItems )
            accepList.ranges.add(new MediaRange(mtype));
        return accepList;
    }

    /**
     * Create a list of accept items from strings.
     * @param acceptStrings
     */
    
    public static AcceptList create(String... acceptStrings) {
        AcceptList accepList = new AcceptList();
        for ( String acceptString : acceptStrings ) {
            accepList.ranges.add(new MediaRange(acceptString));
        }
        return accepList;
    }
    
    /**
     * Parse an HTTP Accept (or etc) header string. 
     * @param headerString
     */
    
    public AcceptList(String headerString) {
        try {
            ranges = stringToAcceptList(headerString);
        }
        catch (Exception ex) {
            ex.printStackTrace(System.err);
            Log.warn(this, "Unrecognized accept string (ignored): " + headerString);
            ranges = new ArrayList<>();
        }
    }

    private /* public */ boolean accepts(MediaRange aItem) {
        return match(aItem) != null;
    }
    
    public List<MediaRange> entries() {
        return Collections.unmodifiableList(ranges);
    }

    private final static MediaRangeCompare comparator = new MediaRangeCompare() ;
    
    /** Find and return a match for a specific MediaType. 
     * Returns the Accept header entry best matched or null.
     */
    public MediaRange match(MediaType offer) {
        // "this" is the client proposal list (Accept header)
        // aItem is one item of the server offer list - a concrete media type (no "*")

        // Search all, find best by specifivity, "q"(quality), and then first occurring if otherwise equal.
        
        MediaRange choice = null ;
        double weight = -1 ;
        int exact = -1 ;
        
        for ( MediaRange acceptItem : ranges ) {
            if ( acceptItem.accepts(offer) ) {
                boolean newChoice = false;
                if ( choice == null ) 
                    // First possibility.
                    newChoice = true ;
                else if ( weight < acceptItem.get_q() )
                    // New possibility has greater weight
                    newChoice = true ;
                else if ( weight == acceptItem.get_q() && exact < acceptItem.subweight() ) 
                    // New possibility has same weight but better exactness.
                    newChoice = true ;
                
                if ( newChoice ) {
                    choice = acceptItem;
                    weight = acceptItem.get_q();
                    exact = acceptItem.subweight();
                    continue;
                }
                //if ( weight == acceptItem.get_q() && !exact &&  
            }
        }
        if ( choice == null )
            return null ;
        return choice ;
    }

    /** Find the best thing in offer list 8sever side) matching the proposal (client, Accept header). 
     *  "best" means highest q value, with left most being better for same q.
     * 
     * @param proposalList Client list of possibilities
     * @param offerList    Server list of possibilities
     * @return MediaType
     */
    
    static public MediaType match(AcceptList proposalList, AcceptList offerList) {
        MediaRange cause = null;
        MediaRange choice = null ;  // From the proposalList
        double weight = -1 ;
        int exactness = -1 ;

        for ( MediaRange offer : offerList.entries() ) {
            MediaRange m = proposalList.match(offer);
            if ( m == null )
                continue;
            boolean newChoice = false;
            
            if ( choice == null )
                newChoice = true ;
            else if ( weight < m.get_q() )
                newChoice = true ;
            else if ( weight == m.get_q() && ( exactness < m.subweight() ) )
                newChoice = true ;
            
            if ( newChoice ) {
                choice = offer ;
                weight = m.get_q() ;
                exactness = m.subweight() ;
            }
        }
        
        if ( choice == null )
            return null ;
        return new MediaType(choice);
    }
    
    public MediaRange first() {
        MediaRange choice = null;
        for ( MediaRange acceptItem : ranges ) {
            if ( choice != null && choice.get_q() >= acceptItem.get_q() )
                continue;
            choice = acceptItem;
        }
        return choice;
    }

    @Override
    public String toString() { return ranges.toString() ; }
    
    private static List<MediaRange> stringToAcceptList(String s) {
        List<MediaRange> ranges = new ArrayList<>();
        if ( s == null )
            return ranges;

        String[] x = s.split(",");
        for ( String aX : x ) {
            if ( aX.equals("") ) {
                continue;
            }
            MediaRange mType = new MediaRange(aX);
            ranges.add(mType);
        }
        return ranges;
    }
    
    private static class MediaRangeCompare implements Comparator<MediaRange> {
        @Override
        public int compare(MediaRange mType1, MediaRange mType2) {
            int r = Double.compare(mType1.get_q(), mType2.get_q()) ;
            
            if ( r == 0 )
                r = subCompare(mType1.getType(), mType2.getType()) ;
            
            if ( r == 0 )
                r = subCompare(mType1.getSubType(), mType2.getSubType()) ;
            
//            if ( r == 0 ) {
//                // This reverses the input order so that the rightmost elements is the
//                // greatest and hence is the first mentioned in the accept range.
//                
//                if ( mType1.posn < mType2.posn )
//                    r = +1 ;
//                if ( mType1.posn > mType2.posn )
//                    r = -1 ;
//            }
            
            // The most significant sorts to the first in a list.
            r = -r ;
            return r ;
        }
        
        public int subCompare(String a, String b) {
            if ( a == null )
                return 1;
            if ( b == null )
                return -1;
            if ( a.equals("*") && b.equals("*") )
                return 0;
            if ( a.equals("*") )
                return -1;
            if ( b.equals("*") )
                return 1;
            return 0;
        }
    }
}
