/*
 * Copyright (c) 2021, NVIDIA CORPORATION.  All rights reserved.
 *
 * 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 mmio

import (
	"fmt"
	"os"
	"syscall"
	"unsafe"

	"github.com/NVIDIA/go-nvlib/pkg/nvpci/bytes"
)

// Mmio memory map a region.
type Mmio interface {
	bytes.Raw
	bytes.Reader
	bytes.Writer
	Sync() error
	Close() error
	Slice(offset int, size int) Mmio
	LittleEndian() Mmio
	BigEndian() Mmio
}

type mmio struct {
	bytes.Bytes
}

func open(path string, offset int, size int, flags int) (Mmio, error) {
	var mmapFlags int
	switch flags {
	case os.O_RDONLY:
		mmapFlags = syscall.PROT_READ
	case os.O_RDWR:
		mmapFlags = syscall.PROT_READ | syscall.PROT_WRITE
	default:
		return nil, fmt.Errorf("invalid flags: %v", flags)
	}

	file, err := os.OpenFile(path, flags, 0)
	if err != nil {
		return nil, fmt.Errorf("failed to open file: %v", err)
	}
	defer file.Close()

	fi, err := file.Stat()
	if err != nil {
		return nil, fmt.Errorf("failed to get file info: %v", err)
	}

	if size > int(fi.Size()) {
		return nil, fmt.Errorf("requested size larger than file size")
	}

	if size < 0 {
		size = int(fi.Size())
	}

	mmap, err := syscall.Mmap(
		int(file.Fd()),
		int64(offset),
		size,
		mmapFlags,
		syscall.MAP_SHARED)
	if err != nil {
		return nil, fmt.Errorf("failed to mmap file: %v", err)
	}

	return &mmio{bytes.New(&mmap)}, nil
}

// OpenRO open region readonly.
func OpenRO(path string, offset int, size int) (Mmio, error) {
	return open(path, offset, size, os.O_RDONLY)
}

// OpenRW open region read write.
func OpenRW(path string, offset int, size int) (Mmio, error) {
	return open(path, offset, size, os.O_RDWR)
}

func (m *mmio) Slice(offset int, size int) Mmio {
	return &mmio{m.Bytes.Slice(offset, size)}
}

func (m *mmio) LittleEndian() Mmio {
	return &mmio{m.Bytes.LittleEndian()}
}

func (m *mmio) BigEndian() Mmio {
	return &mmio{m.Bytes.BigEndian()}
}

func (m *mmio) Close() error {
	err := syscall.Munmap(*m.Bytes.Raw())
	if err != nil {
		return fmt.Errorf("failed to munmap file: %v", err)
	}
	return nil
}

func (m *mmio) Sync() error {
	_, _, errno := syscall.Syscall(
		syscall.SYS_MSYNC,
		uintptr(unsafe.Pointer(&(*m.Bytes.Raw())[0])),
		uintptr(m.Len()),
		uintptr(syscall.MS_SYNC|syscall.MS_INVALIDATE))
	if errno != 0 {
		return fmt.Errorf("failed to msync file: %v", errno)
	}
	return nil
}
