//go:build !go1.20

// Copyright 2016-2022, Pulumi Corporation.
//
// 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 client

import (
	"bytes"
	"encoding/json"
	"io"
	"math"

	jsoniter "github.com/json-iterator/go"

	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
)

const maxNewLines = 1024

var jsonIterConfig = jsoniter.Config{SortMapKeys: true}.Froze()

// Marshals to canonical JSON in the apitype.UntypedDeployment format.
//
// Optimized for large checkpoints.
//
// Injects newlines to allow efficient textual diffs over the JSON. Textual diffs currently use O(N^2) memory in the
// number of newlines, so the injection needs to be conservative. Currently it limits to up to maxNewLines newlines
// which would result in max 8MB memory use by the algorithm.
func MarshalUntypedDeployment(b *bytes.Buffer, deployment *apitype.DeploymentV3) error {
	md := &marshalUntypedDeployment{deployment}
	return md.Write(b)
}

func marshalDeployment(d *apitype.DeploymentV3) (json.RawMessage, error) {
	var b bytes.Buffer
	if err := MarshalUntypedDeployment(&b, d); err != nil {
		return nil, err
	}
	return json.RawMessage(b.Bytes()), nil
}

func marshalVerbatimCheckpointRequest(req apitype.PatchUpdateVerbatimCheckpointRequest) (json.RawMessage, error) {
	// Unlike encoding/json, using jsonIter here will not reindent req.UntypedDeployment, which is what is needed
	// for the Verbatim protocol.
	return jsonIterConfig.Marshal(req)
}

type marshalUntypedDeployment struct {
	deployment *apitype.DeploymentV3
}

func (c *marshalUntypedDeployment) Write(w io.Writer) error {
	cfg := jsonIterConfig
	stream := cfg.BorrowStream(w)
	defer cfg.ReturnStream(stream)
	err := c.writeToStream(stream)
	return err
}

func (c *marshalUntypedDeployment) writeToStream(stream *jsoniter.Stream) error {
	stream.WriteObjectStart()          // writes `{`
	stream.WriteObjectField("version") // writes `"version":`
	stream.WriteInt(3)
	stream.WriteMore() // writes `,`
	stream.WriteObjectField("deployment")
	err := c.writeDeploymentV3(stream)
	if err != nil {
		return err
	}
	stream.WriteObjectEnd() // writes `}`
	return stream.Flush()
}

func (c *marshalUntypedDeployment) writeDeploymentV3(stream *jsoniter.Stream) (err error) {
	deployment := c.deployment
	stream.WriteObjectStart()
	stream.WriteObjectField("manifest")
	stream.WriteVal(deployment.Manifest)
	if deployment.SecretsProviders != nil {
		stream.WriteMore()
		stream.WriteObjectField("secrets_providers")
		stream.WriteVal(deployment.SecretsProviders)
	}
	if err = stream.Flush(); err != nil {
		return err
	}
	nResources := len(deployment.Resources)

	maxNL := maxNewLines - 2
	newlinePeriod := int(math.Ceil(float64(nResources) / float64(maxNL)))

	if nResources > 0 {
		stream.WriteMore()
		stream.WriteObjectField("resources")
		stream.WriteRaw("[\n")
		for i, r := range deployment.Resources {
			if i > 0 {
				stream.WriteRaw(",")
				if (nResources <= maxNL) || (i%newlinePeriod == 0) {
					stream.WriteRaw("\n")
				}
			}
			stream.WriteVal(r)
			if err = stream.Flush(); err != nil {
				return err
			}
		}
		stream.WriteRaw("\n]")
	}
	if len(deployment.PendingOperations) > 0 {
		stream.WriteMore()
		stream.WriteObjectField("pendingOperations")
		stream.WriteVal(deployment.PendingOperations)
	}
	stream.WriteObjectEnd()
	return stream.Flush()
}