/*
 * Copyright (c) 2021 Callum Newlands
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     The additional term of 7.b applies: Requiring preservation of specified
 *     reasonable legal notices or author attributions in that material or in the
 *     Appropriate Legal Notices displayed by works containing it
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package lsystems;

import java.util.List;

import static lsystems.modules.DefinedModules.F;
import static lsystems.modules.DefinedModules.LB;
import static lsystems.modules.DefinedModules.MI;
import static lsystems.modules.DefinedModules.PL;
import static lsystems.modules.DefinedModules.RB;
import static org.junit.jupiter.api.Assertions.assertEquals;

import lsystems.modules.CharModule;
import lsystems.modules.Module;
import lsystems.modules.ParametricExpressionModule;
import lsystems.modules.ParametricParameterModule;
import lsystems.modules.ParametricValueModule;
import org.junit.jupiter.api.Test;

class LSystemTest {

	@Test
	public void LindenmayerAlgae() {
		CharModule A = new CharModule('A');
		CharModule B = new CharModule('B');
		LSystem ls = new LSystem(
				List.of(A),
				List.of(),
				List.of(new ProductionBuilder(
								List.of(A),
								List.of(A, B)
						).build(),
						new ProductionBuilder(
								List.of(B),
								List.of(A)
						).build()
				));
		assertEquals("A", ls.getStateSting());
		assertEquals("AB", ls.performDerivationStep());
		assertEquals("ABA", ls.performDerivationStep());
		assertEquals("ABAAB", ls.performDerivationStep());
		assertEquals("ABAABABA", ls.performDerivationStep());
		assertEquals("ABAABABAABAAB", ls.performDerivationStep());
		assertEquals("ABAABABAABAABABAABABA", ls.performDerivationStep());
		assertEquals("ABAABABAABAABABAABABAABAABABAABAAB", ls.performDerivationStep());

	}

	@Test
	public void SimpleBranching() {
		CharModule A = new CharModule('A');
		CharModule B = new CharModule('B');
		LSystem ls = new LSystem(
				List.of(A),
				List.of(),
				List.of(new ProductionBuilder(
								List.of(B),
								List.of(B, B)
						).build(),
						new ProductionBuilder(
								List.of(A),
								List.of(B, LB, A, RB, A)
						).build()
				));
		assertEquals("A", ls.getStateSting());
		assertEquals("B[A]A", ls.performDerivationStep());
		assertEquals("BB[B[A]A]B[A]A", ls.performDerivationStep());
		assertEquals("BBBB[BB[B[A]A]B[A]A]BB[B[A]A]B[A]A", ls.performDerivationStep());
	}

	@Test
	public void MultipleBranching() {
		CharModule X = new CharModule('X');
		LSystem ls = new LSystem(
				List.of(X),
				List.of(),
				List.of(new ProductionBuilder(
								List.of(X),
								List.of(F, LB, PL, X, RB, LB, MI, X, RB, F, X)
						).build(),
						new ProductionBuilder(
								List.of(F),
								List.of(F, F)
						).build()
				));
		assertEquals("X", ls.getStateSting());
		assertEquals("F[+X][-X]FX", ls.performDerivationStep());
		assertEquals("FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX", ls.performDerivationStep());
		assertEquals("FFFF[+FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX]" +
				"[-FF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX]" +
				"FFFFFF[+F[+X][-X]FX][-F[+X][-X]FX]FFF[+X][-X]FX", ls.performDerivationStep());
	}

	@Test
	public void KochCurve() {
		LSystem ls = new LSystem(
				List.of(F),
				List.of(),
				List.of(new ProductionBuilder(
								List.of(F),
								List.of(F, PL, F, MI, F, MI, F, PL, F)
						).build()
				));
		assertEquals("F", ls.getStateSting());
		assertEquals("F+F-F-F+F", ls.performDerivationStep());
		assertEquals("F+F-F-F+F+F+F-F-F+F-F+F-F-F+F-F+F-F-F+F+F+F-F-F+F", ls.performDerivationStep());
		assertEquals("F+F-F-F+F+F+F-F-F+F-F+F-F-F+F-F+F-F-F+F+F+F-F-F+F+" +
				"F+F-F-F+F+F+F-F-F+F-F+F-F-F+F-F+F-F-F+F+F+F-F-F+F-" +
				"F+F-F-F+F+F+F-F-F+F-F+F-F-F+F-F+F-F-F+F+F+F-F-F+F-" +
				"F+F-F-F+F+F+F-F-F+F-F+F-F-F+F-F+F-F-F+F+F+F-F-F+F+" +
				"F+F-F-F+F+F+F-F-F+F-F+F-F-F+F-F+F-F-F+F+F+F-F-F+F", ls.performDerivationStep());
	}

	@Test
	public void simpleParametricLSystem() {
		ParametricParameterModule Bin = new ParametricParameterModule('B', List.of("x"));
		Module Bout = new ParametricExpressionModule('B', List.of("x"), vars -> List.of(vars.get("x") + 1));
		LSystem ls = new LSystem(
				List.of(new ParametricValueModule('B', 0f)),
				List.of(),
				List.of(new ProductionBuilder(List.of(Bin), List.of(Bout)).build()
				));

		assertEquals("B(0.0)", ls.getStateSting());
		assertEquals("B(1.0)", ls.performDerivationStep());
		assertEquals("B(2.0)", ls.performDerivationStep());
		assertEquals("B(3.0)", ls.performDerivationStep());
		assertEquals("B(4.0)", ls.performDerivationStep());
		assertEquals("B(5.0)", ls.performDerivationStep());
		assertEquals("B(6.0)", ls.performDerivationStep());
		assertEquals("B(7.0)", ls.performDerivationStep());
		assertEquals("B(8.0)", ls.performDerivationStep());
		assertEquals("B(9.0)", ls.performDerivationStep());
		assertEquals("B(10.0)", ls.performDerivationStep());
	}

	@Test
	public void moreComplexParametricLSystem() {
		ParametricParameterModule Ain = new ParametricParameterModule('A', List.of("x", "y"));
		Module Aout1 = new ParametricExpressionModule('A', List.of("x", "y"),
				vars -> List.of(vars.get("x") * 2, vars.get("x") + vars.get("y")));
		Module Aout2 = new ParametricExpressionModule('B', List.of("x"),
				vars -> List.of(vars.get("x")));
		ParametricParameterModule Bin = new ParametricParameterModule('B', List.of("x"));
		Module Bout = new ParametricExpressionModule('B', List.of("x"), vars -> List.of(vars.get("x") - 1));
		LSystem ls = new LSystem(
				List.of(new ParametricValueModule('B', 2f),
						new ParametricValueModule('A', 4f, 2f)),
				List.of(),
				List.of(new ProductionBuilder(List.of(Bin), List.of(Bout)).build(),
						new ProductionBuilder(List.of(Ain), List.of(Aout1, Aout2)).build()
				));

		assertEquals("B(2.0)A(4.0,2.0)", ls.getStateSting());
		assertEquals("B(1.0)A(8.0,6.0)B(4.0)", ls.performDerivationStep());
		assertEquals("B(0.0)A(16.0,14.0)B(8.0)B(3.0)", ls.performDerivationStep());
		assertEquals("B(-1.0)A(32.0,30.0)B(16.0)B(7.0)B(2.0)", ls.performDerivationStep());
		assertEquals("B(-2.0)A(64.0,62.0)B(32.0)B(15.0)B(6.0)B(1.0)", ls.performDerivationStep());
		assertEquals("B(-3.0)A(128.0,126.0)B(64.0)B(31.0)B(14.0)B(5.0)B(0.0)", ls.performDerivationStep());
	}

	// L system from Figure 2.8 of 'Algorithmic Beauty of Plants' by Prusinkiewicz and Lindenmayer
	@Test
	public void canHandleABoP_2_8() {
		/*
			Param values used for fig 2.8:
			Fig d1 		d2 		a 		lr 		T (tropism force)	e 		n
			a 	94.74 	132.63 	18.95 	1.109 	0.00,-1.00,0.00 	0.22 	6
			b 	137.50 	137.50 	18.95 	1.109 	0.00,-1.00,0.00 	0.14 	8
			c 	112.50 	157.50 	22.50 	1.790 	-0.02,-1.00,0.00 	0.27 	8
			d 	180.00 	252.00 	36.00 	1.070 	-0.61,0.77,-0.19 	0.40 	6
		 */

		float d1 = 94.74f;
		float d2 = 132.63f;
		float a = 18.95f;
		float lr = 1.109f;
		float vr = 1.732f;

		CharModule A = new CharModule('A');
		ParametricParameterModule ExIn = new ParametricParameterModule('!', List.of("w"));
		Module ExOut = new ParametricExpressionModule('!', List.of("w"), vars -> List.of(vars.get("w") * vr));
		ParametricParameterModule FIn = new ParametricParameterModule('F', List.of("l"));
		Module FOut = new ParametricExpressionModule('F', List.of("l"), vars -> List.of(vars.get("l") * lr));

		LSystem ls = new LSystem(
				List.of(
						new ParametricValueModule('!', 1f),
						new ParametricValueModule('F', 200f),
						new ParametricValueModule('/', 45f),
						A
				),
				List.of(),
				List.of(
						new ProductionBuilder(List.of(A), List.of(
								new ParametricValueModule('!', vr),
								new ParametricValueModule('F', 50f),
								LB,
								new ParametricValueModule('&', a),
								new ParametricValueModule('F', 50f),
								A,
								RB,
								new ParametricValueModule('/', d1),
								LB,
								new ParametricValueModule('&', a),
								new ParametricValueModule('F', 50f),
								A,
								RB,
								new ParametricValueModule('/', d2),
								new ParametricValueModule('&', a),
								new ParametricValueModule('F', 50f),
								A
						)).build(),
						new ProductionBuilder(List.of(FIn), List.of(FOut)).build(),
						new ProductionBuilder(List.of(ExIn), List.of(ExOut)).build()
				));

		// Checks no errors thrown in 10 derivations steps
		for (int i = 0; i < 10; i++) {
			ls.performDerivationStep();
		}
	}

	@Test
	public void simpleContext() {
		CharModule A = new CharModule('A');
		CharModule B = new CharModule('B');
		LSystem ls = new LSystem(
				List.of(B, A, A, A, A, A, A, A, A),
				List.of(),
				List.of(new ProductionBuilder(
								List.of(A),
								List.of(B)
						).withLeftContext(List.of(B)).build(),
						new ProductionBuilder(
								List.of(B),
								List.of(A)
						).build()
				));
		assertEquals("BAAAAAAAA", ls.getStateSting());
		assertEquals("ABAAAAAAA", ls.performDerivationStep());
		assertEquals("AABAAAAAA", ls.performDerivationStep());
		assertEquals("AAABAAAAA", ls.performDerivationStep());
		assertEquals("AAAABAAAA", ls.performDerivationStep());
		assertEquals("AAAAABAAA", ls.performDerivationStep());
		assertEquals("AAAAAABAA", ls.performDerivationStep());
		assertEquals("AAAAAAABA", ls.performDerivationStep());
		assertEquals("AAAAAAAAB", ls.performDerivationStep());

	}

	@Test
	public void ignoredContext() {
		CharModule A = new CharModule('A');
		CharModule B = new CharModule('B');
		LSystem ls = new LSystem(
				List.of(A, PL, A, A, MI, A, A, PL, A, B),
				List.of(MI, PL),
				List.of(new ProductionBuilder(
								List.of(A),
								List.of(B)
						).withRightContext(List.of(B)).build()
				));
		assertEquals("A+AA-AA+AB", ls.getStateSting());
		assertEquals("A+AA-AA+BB", ls.performDerivationStep());
		assertEquals("A+AA-AB+BB", ls.performDerivationStep());
		assertEquals("A+AA-BB+BB", ls.performDerivationStep());
		assertEquals("A+AB-BB+BB", ls.performDerivationStep());
		assertEquals("A+BB-BB+BB", ls.performDerivationStep());
		assertEquals("B+BB-BB+BB", ls.performDerivationStep());
		assertEquals("B+BB-BB+BB", ls.performDerivationStep());

	}
}