// 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 array_test

import (
	"fmt"
	"math/big"
	"testing"

	"github.com/apache/arrow-go/v18/arrow"
	"github.com/apache/arrow-go/v18/arrow/array"
	"github.com/apache/arrow-go/v18/arrow/bitutil"
	"github.com/apache/arrow-go/v18/arrow/decimal128"
	"github.com/apache/arrow-go/v18/arrow/decimal256"
	"github.com/apache/arrow-go/v18/arrow/memory"
	"github.com/stretchr/testify/suite"
)

type decimalValue interface{}

func bitmapFromSlice(vals []bool) []byte {
	out := make([]byte, int(bitutil.BytesForBits(int64(len(vals)))))
	writer := bitutil.NewBitmapWriter(out, 0, len(vals))
	for _, val := range vals {
		if val {
			writer.Set()
		} else {
			writer.Clear()
		}
		writer.Next()
	}
	writer.Finish()
	return out
}

type DecimalTestSuite struct {
	suite.Suite

	dt  arrow.DataType
	mem *memory.CheckedAllocator
}

func (d *DecimalTestSuite) SetupTest() {
	d.mem = memory.NewCheckedAllocator(memory.DefaultAllocator)
}

func (d *DecimalTestSuite) TearDownTest() {
	d.mem.AssertSize(d.T(), 0)
}

func (d *DecimalTestSuite) makeData(input []decimalValue, out []byte) {
	switch d.dt.ID() {
	case arrow.DECIMAL128:
		for _, v := range input {
			arrow.Decimal128Traits.PutValue(out, v.(decimal128.Num))
			out = out[arrow.Decimal128SizeBytes:]
		}
	case arrow.DECIMAL256:
		for _, v := range input {
			arrow.Decimal256Traits.PutValue(out, v.(decimal256.Num))
			out = out[arrow.Decimal256SizeBytes:]
		}
	}
}

func (d *DecimalTestSuite) testCreate(bitWidth int, prec int32, draw []decimalValue, valids []bool, offset int64) arrow.Array {
	switch bitWidth {
	case 128:
		d.dt = &arrow.Decimal128Type{Precision: prec, Scale: 4}
	case 256:
		d.dt = &arrow.Decimal256Type{Precision: prec, Scale: 4}
	}

	bldr := array.NewBuilder(d.mem, d.dt)
	defer bldr.Release()
	bldr.Reserve(len(draw))

	nullCount := 0
	for i, b := range valids {
		if b {
			switch v := draw[i].(type) {
			case decimal128.Num:
				bldr.(*array.Decimal128Builder).Append(v)
			case decimal256.Num:
				bldr.(*array.Decimal256Builder).Append(v)
			}
		} else {
			bldr.AppendNull()
			nullCount++
		}
	}

	arr := bldr.NewArray()
	d.EqualValues(0, bldr.Len())

	rawBytes := make([]byte, len(draw)*(d.dt.(arrow.FixedWidthDataType).BitWidth()/8))
	d.makeData(draw, rawBytes)

	expectedData := memory.NewBufferBytes(rawBytes)
	expectedNullBitmap := bitmapFromSlice(valids)
	expectedNullCount := len(draw) - bitutil.CountSetBits(expectedNullBitmap, 0, len(valids))

	expected := array.NewData(d.dt, len(valids), []*memory.Buffer{memory.NewBufferBytes(expectedNullBitmap), expectedData}, nil, expectedNullCount, 0)
	defer expected.Release()

	expectedArr := array.MakeFromData(expected)
	defer expectedArr.Release()

	lhs := array.NewSlice(arr, offset, int64(arr.Len())-offset)
	rhs := array.NewSlice(expectedArr, offset, int64(expectedArr.Len())-offset)
	defer func() {
		lhs.Release()
		rhs.Release()
	}()

	d.Truef(array.Equal(lhs, rhs), "expected: %s, got: %s\n", rhs, lhs)
	return arr
}

type Decimal128TestSuite struct {
	DecimalTestSuite
}

func (d *Decimal128TestSuite) runTest(f func(prec int32)) {
	for prec := int32(1); prec <= 38; prec++ {
		d.Run(fmt.Sprintf("prec=%d", prec), func() { f(prec) })
	}
}

func (d *Decimal128TestSuite) TestNoNulls() {
	d.runTest(func(prec int32) {
		draw := []decimalValue{decimal128.FromU64(1), decimal128.FromI64(-2),
			decimal128.FromU64(2389), decimal128.FromU64(4),
			decimal128.FromI64(-12348)}
		valids := []bool{true, true, true, true, true}
		arr := d.testCreate(128, prec, draw, valids, 0)
		arr.Release()
		arr = d.testCreate(128, prec, draw, valids, 2)
		arr.Release()
	})
}

func (d *Decimal128TestSuite) TestWithNulls() {
	d.runTest(func(prec int32) {
		draw := []decimalValue{decimal128.FromU64(1), decimal128.FromU64(2),
			decimal128.FromI64(-1), decimal128.FromI64(4), decimal128.FromI64(-1),
			decimal128.FromI64(1), decimal128.FromI64(2)}
		bigVal, _ := (&big.Int{}).SetString("230342903942234234", 10)
		draw = append(draw, decimal128.FromBigInt(bigVal))

		bigNeg, _ := (&big.Int{}).SetString("-23049302932235234", 10)
		draw = append(draw, decimal128.FromBigInt(bigNeg))

		valids := []bool{true, true, false, true, false, true, true, true, true}
		arr := d.testCreate(128, prec, draw, valids, 0)
		arr.Release()
		arr = d.testCreate(128, prec, draw, valids, 2)
		arr.Release()
	})
}

type Decimal256TestSuite struct {
	DecimalTestSuite
}

func (d *Decimal256TestSuite) runTest(f func(prec int32)) {
	for _, prec := range []int32{1, 2, 5, 10, 38, 39, 40, 75, 76} {
		d.Run(fmt.Sprintf("prec=%d", prec), func() { f(prec) })
	}
}

func (d *Decimal256TestSuite) TestNoNulls() {
	d.runTest(func(prec int32) {
		draw := []decimalValue{decimal256.FromU64(1), decimal256.FromI64(-2),
			decimal256.FromU64(2389), decimal256.FromU64(4),
			decimal256.FromI64(-12348)}
		valids := []bool{true, true, true, true, true}
		arr := d.testCreate(256, prec, draw, valids, 0)
		arr.Release()
		arr = d.testCreate(256, prec, draw, valids, 2)
		arr.Release()
	})
}

func (d *Decimal256TestSuite) TestWithNulls() {
	d.runTest(func(prec int32) {
		draw := []decimalValue{decimal256.FromU64(1), decimal256.FromU64(2),
			decimal256.FromI64(-1), decimal256.FromI64(4), decimal256.FromI64(-1),
			decimal256.FromI64(1), decimal256.FromI64(2)}

		// (pow(2, 255) - 1)
		bigVal, _ := (&big.Int{}).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819967", 10)
		draw = append(draw, decimal256.FromBigInt(bigVal))

		draw = append(draw, decimal256.FromBigInt(bigVal.Neg(bigVal)))

		valids := []bool{true, true, false, true, false, true, true, true, true}
		arr := d.testCreate(256, prec, draw, valids, 0)
		arr.Release()
		arr = d.testCreate(256, prec, draw, valids, 2)
		arr.Release()
	})
}

func TestDecimal(t *testing.T) {
	suite.Run(t, new(Decimal128TestSuite))
	suite.Run(t, new(Decimal256TestSuite))
}
