/*
 * 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.cassandra.sidecar.common.utils;

import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.cassandra.sidecar.common.exceptions.RangeException;
import org.jetbrains.annotations.NotNull;

/**
 * Accepted Range formats are start-end, start-, -suffix_length
 * start-end (start = start index of the range, end = end index of the range, both inclusive)
 * start- (start = start index of the range, end = end of file)
 * -suffix-length (Requested length from end of file. The length should be positive)
 */
public class HttpRange
{
    private static final String RANGE_UNIT = "bytes";
    // matches a. bytes=1-2, b. bytes=1-, c. bytes=-2, d. bytes=-. Need to do another valid for case d.
    private static final Pattern RANGE_HEADER = Pattern.compile("^" + RANGE_UNIT + "=(\\d*)-(\\d*)$");
    private static final long BOUND_ABSENT = -1L;
    private final long start;
    private final long end;
    private final long length;

    // An initialized range is always valid; invalid params fail range initialization.
    private HttpRange(final long start, final long end)
    {
        Preconditions.checkArgument(start >= 0,
                                    () -> String.format("Range start can not be negative. range=[%s, %s]", start, end));
        Preconditions.checkArgument(end >= start,
                                    () -> String.format("Range does not satisfy boundary requirements. range=[%s, %s]", start, end));
        this.start = start;
        this.end = end;
        long len = end - start + 1; // Assign long max if overflows
        this.length = len < 0 ? Long.MAX_VALUE : len;
    }

    /**
     * Accepted RangeHeader formats are bytes=start-end, bytes=start-, bytes=-suffix_length
     *
     * @param header   the header string to be parsed
     * @param fileSize the size of the file
     * @return the parsed HTTP range header
     */
    public static HttpRange parseHeader(final String header, final long fileSize)
    {
        if (header == null)
        {
            return new HttpRange(0, fileSize - 1);
        }
        return HttpRange.parse(header, fileSize);
    }

    public static HttpRange of(final long start, final long end)
    {
        return new HttpRange(start, end);
    }

    /**
     * Accepted string formats "bytes=1453-3563", "bytes=-22344", "bytes=5346-"
     * Sample invalid string formats "bytes=8-3", "bytes=-", "bytes=-0", "bytes=a-b"
     *
     * @param fileSize - passed in to convert partial range into absolute range
     */
    private static HttpRange parse(@NotNull String rangeHeader, final long fileSize)
    {
        Matcher m = RANGE_HEADER.matcher(rangeHeader);
        if (!m.matches())
        {
            throw invalidRangeHeaderException(rangeHeader);
        }

        long left = parseLong(m.group(1), rangeHeader);
        long right = parseLong(m.group(2), rangeHeader);

        if (left == BOUND_ABSENT && right == BOUND_ABSENT) // matching "bytes=-"
        {
            throw invalidRangeHeaderException(rangeHeader);
        }
        else if (left == BOUND_ABSENT) // matching "bytes=-1"
        {
            long len = Math.min(right, fileSize); // correct the range if it exceeds file size.
            return new HttpRange(fileSize - len, fileSize - 1);
        }
        else if (right == BOUND_ABSENT) // matching "bytes=1-"
        {
            return new HttpRange(left, fileSize - 1);
        }
        else
        {
            return new HttpRange(left, right);
        }
    }

    // return -1 for empty string; return long value otherwise.
    // throws IllegalArgumentException for invalid value string
    private static long parseLong(String valStr, String rangeHeader)
    {
        if (valStr == null || valStr.isEmpty())
        {
            return BOUND_ABSENT;
        }

        try
        {
            return Long.parseLong(valStr);
        }
        catch (NumberFormatException e)
        {
            throw invalidRangeHeaderException(rangeHeader);
        }
    }

    private static IllegalArgumentException invalidRangeHeaderException(String rangeHeader)
    {
        return new IllegalArgumentException("Invalid range header: " + rangeHeader + ". " +
                                            "Supported Range formats are bytes=<start>-<end>, " +
                                            "bytes=<start>-, bytes=-<suffix-length>");
    }

    public long start()
    {
        return this.start;
    }

    public long end()
    {
        return this.end;
    }

    public long length()
    {
        return this.length;
    }

    public HttpRange intersect(@NotNull final HttpRange range)
    {
        long newStart = Math.max(start, range.start());
        long newEnd = Math.min(end, range.end());
        if (newStart > newEnd)
        {
            throw new RangeException("Range does not overlap");
        }

        return new HttpRange(newStart, newEnd);
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o)
        {
            return true;
        }
        if (!(o instanceof HttpRange))
        {
            return false;
        }
        HttpRange range = (HttpRange) o;
        return start == range.start &&
               end == range.end &&
               length == range.length;
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(start, end, length);
    }

    @Override
    public String toString()
    {
        return String.format("%s=%d-%d", RANGE_UNIT, start, end);
    }
}
