/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 io.undertow.util;

import io.undertow.UndertowLogger;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Represents a byte range for a range request
 *
 *
 * @author Stuart Douglas
 */
public class ByteRange {

    private final List<Range> ranges;

    public ByteRange(List<Range> ranges) {
        this.ranges = ranges;
    }

    public int getRanges() {
        return ranges.size();
    }

    /**
     * Gets the start of the specified range segment, of -1 if this is a suffix range segment
     * @param range The range segment to get
     * @return The range start
     */
    public long getStart(int range) {
        return ranges.get(range).getStart();
    }

    /**
     * Gets the end of the specified range segment, or the number of bytes if this is a suffix range segment
     * @param range The range segment to get
     * @return The range end
     */
    public long getEnd(int range) {
        return ranges.get(range).getEnd();
    }

    /**
     * Attempts to parse a range request. If the range request is invalid it will just return null so that
     * it may be ignored.
     *
     *
     * @param rangeHeader The range spec
     * @return A range spec, or null if the range header could not be parsed
     */
    public static ByteRange parse(String rangeHeader) {
        if(rangeHeader == null || rangeHeader.length() < 7) {
            return null;
        }
        if(!rangeHeader.startsWith("bytes=")) {
            return null;
        }
        List<Range> ranges = new ArrayList<>();
        String[] parts = rangeHeader.substring(6).split(",");
        for(String part : parts) {
            try {
                int index = part.indexOf('-');
                if (index == 0) {
                    //suffix range spec
                    //represents the last N bytes
                    //internally we represent this using a -1 as the start position
                    long val = Long.parseLong(part.substring(1));
                    if(val < 0) {
                        UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
                        return null;
                    }
                    ranges.add(new Range(-1, val));
                } else {
                    if(index == -1) {
                        UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
                        return null;
                    }
                    long start = Long.parseLong(part.substring(0, index));
                    if(start < 0) {
                        UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
                        return null;
                    }
                    long end;
                    if (index + 1 < part.length()) {
                        end = Long.parseLong(part.substring(index + 1));
                    } else {
                        end = -1;
                    }
                    ranges.add(new Range(start, end));
                }
            } catch (NumberFormatException e) {
                UndertowLogger.REQUEST_LOGGER.debugf("Invalid range spec %s", rangeHeader);
                return null;
            }
        }
        if(ranges.isEmpty()) {
            return null;
        }
        return new ByteRange(ranges);
    }

    /**
     * Returns a representation of the range result. If this returns null then a 200 response should be sent instead
     * @param resourceContentLength
     * @return
     */
    public RangeResponseResult getResponseResult(final long resourceContentLength, String ifRange, Date lastModified, String eTag) {
        if(ranges.isEmpty()) {
            return null;
        }
        long start = getStart(0);
        long end = getEnd(0);
        long rangeLength;
        if(ifRange != null && !ifRange.isEmpty()) {
            if(ifRange.charAt(0) == '"') {
                //entity tag
                if(eTag != null && !eTag.equals(ifRange)) {
                    return null;
                }
            } else {
                Date ifDate = DateUtils.parseDate(ifRange);
                if(ifDate != null && lastModified != null && ifDate.getTime() < lastModified.getTime()) {
                    return null;
                }
            }
        }

        if(start == -1 ) {
            //suffix range
            if(end < 0){
                //ignore the range request
                return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE);
            }
            start = Math.max(resourceContentLength - end, 0);
            end = resourceContentLength - 1;
            rangeLength = resourceContentLength - start;
        } else if(end == -1) {
            //prefix range
            long toWrite = resourceContentLength - start;
            if (toWrite > 0) {
                rangeLength = toWrite;
            } else {
                //ignore the range request
                return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE);
            }
            end = resourceContentLength - 1;
        } else {
            end = Math.min(end, resourceContentLength - 1);
            if(start >= resourceContentLength || start > end) {
                return new RangeResponseResult(0, 0, 0, "bytes */" + resourceContentLength, StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE);
            }
            rangeLength = end - start + 1;
        }
        return new RangeResponseResult(start, end, rangeLength,  "bytes " + start + "-" + end + "/" + resourceContentLength, StatusCodes.PARTIAL_CONTENT);
    }

    public static class RangeResponseResult {
        private final long start;
        private final long end;
        private final long contentLength;
        private final String contentRange;
        private final int statusCode;

        public RangeResponseResult(long start, long end, long contentLength, String contentRange, int statusCode) {
            this.start = start;
            this.end = end;
            this.contentLength = contentLength;
            this.contentRange = contentRange;
            this.statusCode = statusCode;
        }

        public long getStart() {
            return start;
        }

        public long getEnd() {
            return end;
        }

        public long getContentLength() {
            return contentLength;
        }

        public String getContentRange() {
            return contentRange;
        }

        public int getStatusCode() {
            return statusCode;
        }
    }

    public static class Range {
        private final long start, end;

        public Range(long start, long end) {
            this.start = start;
            this.end = end;
        }

        public long getStart() {
            return start;
        }

        public long getEnd() {
            return end;
        }
    }
}
