/*
 * 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 static org.apache.jena.atlas.lib.Lib.hashCodeObject ;

import java.util.LinkedHashMap ;
import java.util.Map ;
import java.util.Objects;

import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/**
 * A structure to represent a <a
 * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7">media
 * type</a>. See also the <a
 * href="http://httpd.apache.org/docs/current/content-negotiation.html">Apache
 * httpd documentation</a>.
 */
public class MediaType {
    private static Logger             log        = LoggerFactory.getLogger(MediaType.class) ;

    private static final String       strCharset = "charset" ;

    private final String              type ;
    private final String              subType ;
    // Keys in insertion order.
    private final Map<String, String> params ;

    protected MediaType(ParsedMediaType parser) {
        this.type = parser.type ;
        this.subType = parser.subType ;
        this.params = parser.params ;
    }

    public MediaType(MediaType other) {
        this.type = other.type ;
        this.subType = other.subType ;
        // Order preserving copy.
        this.params = new LinkedHashMap<>(other.params) ;
    }

    /** Create a media type from type and subType */
    public MediaType(String type, String subType) {
        this(type, subType, null) ;
    }

    /** Create a media type from type and subType */
    public MediaType(String type, String subType, String charset) {
        this.type = type ;
        this.subType = subType ;
        this.params = new LinkedHashMap<>() ;
        if ( charset != null )
            setParameter(strCharset, charset) ;
    }

    public static MediaType create(String contentType, String charset) {
        ParsedMediaType mediaType = parse(contentType) ;
        if ( charset != null )
            mediaType.params.put(strCharset, charset) ;
        return new MediaType(mediaType) ;
    }

    public static MediaType createFromContentType(String string) {
        return new MediaType(parse(string)) ;
    }

    public static MediaType create(String contentType, String subType, String charset) {
        return new MediaType(contentType, subType, charset) ;
    }

    public static MediaType create(String string) {
        if ( string == null )
            return null ;
        return new MediaType(parse(string)) ;
    }

    public static ParsedMediaType parse(String string) {
        ParsedMediaType mt = new ParsedMediaType() ;

        String[] x = WebLib.split(string, ";") ;
        String[] t = WebLib.split(x[0], "/") ;
        mt.type = t[0] ;
        if ( t.length > 1 )
            mt.subType = t[1] ;

        for (int i = 1; i < x.length; i++) {
            // Each a parameter
            String z[] = WebLib.split(x[i], "=") ;
            if ( z.length == 2 )
                mt.params.put(z[0], z[1]) ;
            else
                log.warn("Duff parameter: " + x[i] + " in " + string) ;
        }
        return mt ;
    }

    /** Format for use in HTTP header */

    public String toHeaderString() {
        StringBuilder b = new StringBuilder() ;
        b.append(type) ;
        if ( subType != null )
            b.append("/").append(subType) ;

        for (Map.Entry<String, String> entry : params.entrySet()) {
            b.append(";") ;
            b.append(entry.getKey()) ;
            b.append("=") ;
            b.append(entry.getValue()) ;
        }
        return b.toString() ;
    }

    /**
     * Format to show structure - intentionally different from header form so
     * you can tell parsing happened correctly
     */

    @Override
    public String toString() {
        StringBuilder b = new StringBuilder() ;
        b.append("[") ;
        b.append(type) ;
        if ( subType != null )
            b.append("/").append(subType) ;
        for (String k : params.keySet()) {
            if ( k.equals("boundary") )
                continue ;
            String v = params.get(k) ;
            b.append(" ") ;
            b.append(k) ;
            b.append("=") ;
            b.append(v) ;
        }
        b.append("]") ;
        return b.toString() ;
    }

    // private String type = null ;
    // private String subType = null ;
    // // Keys in insertion order.
    // private Map<String, String> params = new LinkedHashMap<String, String>()
    // ;

    @Override
    public int hashCode() {
        return hashCodeObject(type, 1) ^ hashCodeObject(subType, 2) ^ hashCodeObject(params, 3) ;
    }

    @Override
    public boolean equals(Object object) {
        if ( this == object )
            return true ;
        if ( !(object instanceof MediaType) )
            return false ;
        MediaType mt = (MediaType)object ;
        return Objects.equals(type, mt.type) && Objects.equals(subType, mt.subType) && Objects.equals(params, mt.params) ;
    }

    public String getParameter(String name) {
        return params.get(name) ;
    }

    private void setParameter(String name, String value) {
        params.put(name, value) ;
        strContentType = null ;
    }

    // A cache.
    private String strContentType = null ;

    public String getContentType() {
        if ( strContentType != null )
            return strContentType ;
        if ( subType == null )
            return type ;
        return type + "/" + subType ;
    }

    public String getCharset() {
        return getParameter(strCharset) ;
    }

    public String getSubType() {
        return subType ;
    }

    // public void setSubType(String subType) { this.subType = subType ;
    // strContentType = null ; }
    public String getType() {
        return type ;
    }
    // public void setType(String type) { this.type = type ; strContentType =
    // null ; }

    /**
     * The outcome of parsing
     * 
     * @see MediaType#parse
     */
    /* package */static class ParsedMediaType {
        public String              type ;
        public String              subType ;
        public Map<String, String> params = new LinkedHashMap<>() ;
    }
}
