//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.fcgi.generator;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;

public class ClientGenerator extends Generator
{
    // To keep the algorithm simple, and given that the max length of a
    // frame is 0xFF_FF we allow the max length of a name (or value) to be
    // 0x7F_FF - 4 (the 4 is to make room for the name (or value) length).
    public static final int MAX_PARAM_LENGTH = 0x7F_FF - 4;

    public ClientGenerator(ByteBufferPool byteBufferPool)
    {
        super(byteBufferPool);
    }

    public Result generateRequestHeaders(int request, HttpFields fields, Callback callback)
    {
        request &= 0xFF_FF;

        final Charset utf8 = StandardCharsets.UTF_8;
        List<byte[]> bytes = new ArrayList<>(fields.size() * 2);
        int fieldsLength = 0;
        for (HttpField field : fields)
        {
            String name = field.getName();
            byte[] nameBytes = name.getBytes(utf8);
            if (nameBytes.length > MAX_PARAM_LENGTH)
                throw new IllegalArgumentException("Field name " + name + " exceeds max length " + MAX_PARAM_LENGTH);
            bytes.add(nameBytes);

            String value = field.getValue();
            byte[] valueBytes = value.getBytes(utf8);
            if (valueBytes.length > MAX_PARAM_LENGTH)
                throw new IllegalArgumentException("Field value " + value + " exceeds max length " + MAX_PARAM_LENGTH);
            bytes.add(valueBytes);

            int nameLength = nameBytes.length;
            fieldsLength += bytesForLength(nameLength);

            int valueLength = valueBytes.length;
            fieldsLength += bytesForLength(valueLength);

            fieldsLength += nameLength;
            fieldsLength += valueLength;
        }

        // Worst case FCGI_PARAMS frame: long name + long value - both of MAX_PARAM_LENGTH
        int maxCapacity = 4 + 4 + 2 * MAX_PARAM_LENGTH;

        // One FCGI_BEGIN_REQUEST + N FCGI_PARAMS + one last FCGI_PARAMS

        ByteBuffer beginRequestBuffer = byteBufferPool.acquire(16, false);
        BufferUtil.clearToFill(beginRequestBuffer);
        Result result = new Result(byteBufferPool, callback);
        result = result.append(beginRequestBuffer, true);

        // Generate the FCGI_BEGIN_REQUEST frame
        beginRequestBuffer.putInt(0x01_01_00_00 + request);
        beginRequestBuffer.putInt(0x00_08_00_00);
        // Hardcode RESPONDER role and KEEP_ALIVE flag
        beginRequestBuffer.putLong(0x00_01_01_00_00_00_00_00L);
        BufferUtil.flipToFlush(beginRequestBuffer, 0);

        int index = 0;
        while (fieldsLength > 0)
        {
            int capacity = 8 + Math.min(maxCapacity, fieldsLength);
            ByteBuffer buffer = byteBufferPool.acquire(capacity, true);
            BufferUtil.clearToFill(buffer);
            result = result.append(buffer, true);

            // Generate the FCGI_PARAMS frame
            buffer.putInt(0x01_04_00_00 + request);
            buffer.putShort((short)0);
            buffer.putShort((short)0);
            capacity -= 8;

            int length = 0;
            while (index < bytes.size())
            {
                byte[] nameBytes = bytes.get(index);
                int nameLength = nameBytes.length;
                byte[] valueBytes = bytes.get(index + 1);
                int valueLength = valueBytes.length;

                int required = bytesForLength(nameLength) + bytesForLength(valueLength) + nameLength + valueLength;
                if (required > capacity)
                    break;

                putParamLength(buffer, nameLength);
                putParamLength(buffer, valueLength);
                buffer.put(nameBytes);
                buffer.put(valueBytes);

                length += required;
                fieldsLength -= required;
                capacity -= required;
                index += 2;
            }

            buffer.putShort(4, (short)length);
            BufferUtil.flipToFlush(buffer, 0);
        }

        ByteBuffer lastParamsBuffer = byteBufferPool.acquire(8, false);
        BufferUtil.clearToFill(lastParamsBuffer);
        result = result.append(lastParamsBuffer, true);

        // Generate the last FCGI_PARAMS frame
        lastParamsBuffer.putInt(0x01_04_00_00 + request);
        lastParamsBuffer.putInt(0x00_00_00_00);
        BufferUtil.flipToFlush(lastParamsBuffer, 0);

        return result;
    }

    private int putParamLength(ByteBuffer buffer, int length)
    {
        int result = bytesForLength(length);
        if (result == 4)
            buffer.putInt(length | 0x80_00_00_00);
        else
            buffer.put((byte)length);
        return result;
    }

    private int bytesForLength(int length)
    {
        return length > 127 ? 4 : 1;
    }

    public Result generateRequestContent(int request, ByteBuffer content, boolean lastContent, Callback callback)
    {
        return generateContent(request, content, false, lastContent, callback, FCGI.FrameType.STDIN);
    }
}
