/*
 * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.internal.transport.sshd.agent.connector;

import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.platform.win32.WinDef.LPARAM;
import com.sun.jna.platform.win32.WinDef.LRESULT;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.platform.win32.WinUser;

/**
 * The {@link PageantLibrary} encapsulates the shared memory access and provides
 * a simple pipe abstraction.
 */
public final class PageantLibrary {

	private static final Logger LOG = LoggerFactory
			.getLogger(PageantLibrary.class);

	/** Pageant's "class" and "window name". */
	private static final String PAGEANT = "Pageant"; //$NON-NLS-1$

	/**
	 * Magic constant from Pageant; ID for the CopyStruct used in SendMessage.
	 *
	 * @see <a href=
	 *      "https://git.tartarus.org/?p=simon/putty.git;a=blob;f=windows/pageant.c;h=0e25cc5d48f0#l33">"random goop"</a>
	 */
	private static final int PAGEANT_ID = 0x804e_50ba;

	/**
	 * Determines whether Pageant is currently running.
	 *
	 * @return {@code true} if Pageant is running, {@code false} otherwise
	 */
	boolean isPageantAvailable() {
		LibraryHolder libs = LibraryHolder.getLibrary();
		if (libs == null) {
			return false;
		}
		HWND window = libs.user.FindWindow(PAGEANT, PAGEANT);
		return window != null && !window.equals(WinBase.INVALID_HANDLE_VALUE);
	}

	/**
	 * An abstraction for a bi-directional pipe.
	 */
	interface Pipe extends Closeable {

		/**
		 * Send the given message.
		 *
		 * @param message
		 *            to send
		 * @throws IOException
		 *             on errors
		 */
		void send(byte[] message) throws IOException;

		/**
		 * Reads bytes from the pipe until {@code data} is full.
		 *
		 * @param data
		 *            to read
		 * @throws IOException
		 *             on errors
		 */
		void receive(byte[] data) throws IOException;
	}

	/**
	 * Windows' COPYDATASTRUCT. Must be public for JNA.
	 */
	public static class CopyStruct extends Structure {

		/** Must be set the {@link #PAGEANT_ID}. */
		public int dwData = PAGEANT_ID;

		/** Data length; number of bytes in {@link #lpData}. */
		public long cbData;

		/** Points to {@link #cbData} bytes. */
		public Pointer lpData;

		@Override
		protected List<String> getFieldOrder() {
			return List.of("dwData", "cbData", "lpData"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}
	}

	private static class PipeImpl implements Pipe {

		private final LibraryHolder libs;

		private final HWND window;

		private final byte[] name;

		private final HANDLE file;

		private final Pointer memory;

		private long readPos = 0;

		PipeImpl(LibraryHolder libs, HWND window, String name, HANDLE file,
				Pointer memory) {
			this.libs = libs;
			this.window = window;
			this.name = name.getBytes(StandardCharsets.US_ASCII);
			this.file = file;
			this.memory = memory;
		}

		@Override
		public void close() throws IOException {
			PageantLibrary.close(libs, file, memory, false);
		}

		private Pointer init(CopyStruct c) {
			c.cbData = name.length + 1;
			c.lpData = new Memory(c.cbData);
			c.lpData.write(0, name, 0, name.length);
			c.lpData.setByte(name.length, (byte) 0);
			c.write();
			return c.getPointer();
		}

		@Override
		public void send(byte[] message) throws IOException {
			memory.write(0, message, 0, message.length);
			CopyStruct c = new CopyStruct();
			Pointer p = init(c);
			LRESULT result = libs.user.SendMessage(window, WinUser.WM_COPYDATA,
					null, new LPARAM(Pointer.nativeValue(p)));
			if (result == null || result.longValue() == 0) {
				throw new IOException(
						libs.systemError(Texts.get().msgSendFailed2));
			}
		}

		@Override
		public void receive(byte[] data) throws IOException {
			// Relies on Pageant handling the request synchronously, i.e.,
			// SendMessage() above returning successfully only once Pageant
			// has indeed written into the shared memory.
			memory.read(readPos, data, 0, data.length);
			readPos += data.length;
		}
	}

	/**
	 * Creates a new {@link Pipe}.
	 *
	 * @param name
	 *            for the pipe
	 * @param maxSize
	 *            maximum size for messages
	 * @return the {@link Pipe}, or {@code null} if none created
	 * @throws IOException on errors
	 */
	Pipe createPipe(String name, int maxSize) throws IOException {
		LibraryHolder libs = LibraryHolder.getLibrary();
		if (libs == null) {
			throw new IllegalStateException("Libraries were not loaded"); //$NON-NLS-1$
		}
		HWND window = libs.user.FindWindow(PAGEANT, PAGEANT);
		if (window == null || window.equals(WinBase.INVALID_HANDLE_VALUE)) {
			throw new IOException(Texts.get().msgPageantUnavailable);
		}
		String fileName = name + libs.kernel.GetCurrentThreadId();
		HANDLE file = null;
		Pointer sharedMemory = null;
		try {
			file = libs.kernel.CreateFileMapping(WinBase.INVALID_HANDLE_VALUE,
					null, WinNT.PAGE_READWRITE, 0, maxSize, fileName);
			if (file == null || file.equals(WinBase.INVALID_HANDLE_VALUE)) {
				throw new IOException(
						libs.systemError(Texts.get().msgNoMappedFile));
			}
			sharedMemory = libs.kernel.MapViewOfFile(file,
					WinNT.SECTION_MAP_WRITE, 0, 0, 0);
			if (sharedMemory == null) {
				throw new IOException(
						libs.systemError(Texts.get().msgNoSharedMemory));
			}
			return new PipeImpl(libs, window, fileName, file, sharedMemory);
		} catch (IOException e) {
			close(libs, file, sharedMemory, true);
			throw e;
		} catch (Throwable e) {
			close(libs, file, sharedMemory, true);
			throw new IOException(Texts.get().msgSharedMemoryFailed, e);
		}
	}

	private static void close(LibraryHolder libs, HANDLE file, Pointer memory,
			boolean silent) throws IOException {
		if (memory != null) {
			if (!libs.kernel.UnmapViewOfFile(memory)) {
				String msg = libs
						.systemError(Texts.get().errReleaseSharedMemory);
				if (silent) {
					LOG.error(msg);
				} else {
					throw new IOException(msg);
				}
			}
		}
		if (file != null) {
			if (!libs.kernel.CloseHandle(file)) {
				String msg = libs.systemError(Texts.get().errCloseMappedFile);
				if (silent) {
					LOG.error(msg);
				} else {
					throw new IOException(msg);
				}
			}
		}
	}
}
