MODULE filteredinput INTERFACE;

{*
	Este módulo proporciona funciones y procedimientos para
	obtener datos del usuario.
	
	Todos los procedimientos imprimen sus propios formularios, y
	filtran la entrada de los datos para evitar errores de formato
	y contenido, hablando tanto en términos informáticos como en lo
	relacionado con la utilidad de la aplicación.
*}

export filteredinput = (editStudent, MONTHS, newStudent, readChar, YES, YES_NO);

IMPORT
	
	filesystem;
	
CONST
	
	MONTHS	= ['f', 'j', 's', 'd', 'F', 'J', 'S', 'D'];
	YES		= ['s', 'S'];
	YES_NO	= ['s', 'n', 'S', 'N'];
	
{*
	Maneja la entrada de datos para editar un alumno. Permite
	buscarlo y cambiar todos los datos del mismo. El valor devuelto 
	será 5 si el alumno ha sido editado corréctamente, 6 si no
	se ha cargado ningún alumno o 7 si la base de datos está
	vacía.
*}
function editStudent:integer;

{*
	Pide el login del alumno s^. search indica si se está haciendo
	una búsqueda (true) o no (false), y notNull indica si se permite
	dejar el login en blanco (false, por ejemplo al editar un alumno,
	pues dejar los campos en blanco significa no modificar su valor
	anterior) o no (true).
*}
procedure getStudentLogin(s:PtrToStudent; search, notNull:boolean);

{*
	Pide la nota de la convocatoria option para el alumno
	s^. t indica si es la nota teórica (true) o la práctica (false).
	Para que sea válido, el valor de la nota debe estar entre 0 y
	10 (incluídos) y tener un decimal como máximo.
*}
procedure getStudentMark(s:PtrToStudent; option:char; t:boolean);

{*
	Llama a getStudentMark para pedir las notas teórica y
	práctica de la convocatoria option para el alumno s^.
*}
procedure getStudentMarks(s:PtrToStudent; option:char);

{*
	Pregunta al usuario la convocatoria para la cual
	se pedirán posteriormente las notas. s es un puntero
	al alumno en cuestión.
*}
procedure getStudentMarksQC(s:PtrToStudent);

{*
	Pregunta al usuario si desea introducir las notas para el
	alumno s^. first indica si es la primera vez que se
	hace la pregunta (true) o no (false). La función devuelve
	true si el usuario ha decidido introducirlas, o false
	en el caso contrario.
*}
function getStudentMarksQYN(s:PtrToStudent; first:boolean):boolean;

{*
	Pide el nombre para el alumno s^.
*}
procedure getStudentName(s:PtrToStudent);

{*
	Pide los apellidos para el alumno s^.
*}
procedure getStudentSurname(s:PtrToStudent);

{*
	Maneja la entrada de datos para crear un nuevo alumno. Todos
	los datos son opcionales menos el login, el cual además
	no puede ser el mismo para más de un alumno. La función
	devuelve 3 si se ha podido almacenar el nuevo alumno
	en la base de datos o 4 en caso contrario.
*}
function newStudent:integer;

{*
	Lee un carácter de la entrada estándar, lo
	almacena en c y descarta los posibles
	caracteres que queden en el buffer.
*}
procedure readChar(var c:char);

{*
	Lee una cadena de texto, comprueba que su
	formato sea el adecuado para ser un login
	y lo almacena en l. search indica si debe
	evitar aceptar un login ya existente (false)
	o no (true). La función devuelve 1 si el usuario
	no ha introducido ningún caracter, 2 si el texto
	introducido no tiene 8 caracteres (contando cada
	byte como un caracter), 3 si no tiene el formato
	correcto, 4 si search es false y ya existe un alumno 
	con el mismo login o 0 si todo es correcto.
*}
function readLogin(var l:string; search:boolean):integer;

{*
	Lee de la entrada estándar hacia r, comprobando
	que el texto introducido es un real. Para que sea válido
	y r se llene con el valor introducido por el usuario el
	mismo debe ser un real, estar comprendido entre max
	y min (incluídos) y no tener más de dMax cifras decimales.
	Esta función devolverá 0 si todo se ha llevado a cabo
	correctamente, 1 si el texto introducido no es un real,
	2 si es mayor que max, 3 si es menor que min o 4
	si tiene más cifras decimales que dMax.
*}
function readReal(var r:real; max, min, dMax:integer):integer;

END.


MODULE filteredinput IMPLEMENTATION;

IMPORT
	
	standardinput;
	standardoutput;
	display;
	
CONST
	
	LETTERS		= ['a'..'z'];
	MARK_MAX	= 10;
	MARK_MAX_D	= 1;
	MARK_MIN	= 0;
	MINUS		= '-';
	NINE		= ord('9');
	NUMBERS		= ['0'..'9'];
	POINT		= '.';
	PLUS		= '+';
	ZERO		= ord('0');
	
function editStudent;
var
	
	b:boolean;
	login:string(255);
	result:integer;
	s:PtrToStudent;
	
begin
	
	{*
		data es global, proporcionada por el módulo filesystem.
	*}
	
	reset(data^);
	
	if lastPosition(data^) = -1 then
	begin
		
		editStudent := 7;
		
	end
	else
	begin
		
		clearDisplay;
		setTitle('Editar alumno');
		
		setMssg('Deja en blanco el campo y pulsa intro para volver al menú principal.');
		
		write('Login: ');
		
		result := readLogin(login, true);
		if result = 0 then
		begin
			
			s := searchStudent(login);
			if s = NIL then result := 5;
			
		end;
		
		while result > 1 do
		begin
			
			case result of
				
				2: setError('El login del alumno debe tener exactamente 8 caracteres.');
				3: setError('El formato de "login" debe ser aaaaaann, siendo cada "a" una letra minúscula del alfabeto inglés y cada "n" un número.');
				5: setError('No existe ningún alumno en la base de datos que tenga ese login.');
				
			end;
			
			clearLine;
			write('Login: ');
			
			result := readLogin(login, true);
			
			if result = 0 then
			begin
				
				s := searchStudent(login);
				if s = NIL then result := 5;
				
			end;
			
		end;
		
		if result = 0 then
		begin
			
			downLines(1);
			
			setMssg('Dejar un campo en blanco no modifica el valor anterior del mismo.');
			
			write('Nombre actual: ', s^.name);
			downLines(1);
			getStudentName(s);
			downLines(1);
			
			write('Apellidos actuales: ', s^.surname);
			downLines(1);
			getStudentSurname(s);
			downLines(1);
			
			write('Login actual: ', s^.login);
			downLines(1);
			getStudentLogin(s, true, false);
			downLines(1);
			
			memoLine;
			b := getStudentMarksQYN(s, true);
			while b do
			begin
				
				returnToLine;
				clearDown;
				b := getStudentMarksQYN(s, false);
				
			end;
			
			
			{*
				beingEdited es global, proporcionada por filesystem.
			*}
			seekUpdate(data^, beingEdited);
			data^^ := s^;
			update(data^);
			
			dispose(s);
			
			editStudent := 5;
			
		end
		else editStudent := 6;
		
	end;
	
end;

procedure getStudentLogin;
var
	
	login:string(255);
	option, result:integer;
	
begin
	
	write('Login: ');
	
	if notNull then option := 0
	else option := 1;
	
	result := readLogin(login, search);
	while result > option do
	begin
		
		case result of
			
			1: setError('El login no puede dejarse en blanco.');
			2: setError('El login del alumno debe tener exactamente 8 caracteres.');
			3: setError('El formato de "login" debe ser aaaaaann, siendo cada "a" una letra minúscula del alfabeto inglés y cada "n" un número.');
			4: setError('Ya existe un alumno con ese login. Por favor, utilice uno distinto.');
			
		end;
		
		clearLine;
		
		write('Login: ');
		
		result := readLogin(login, search);
		
	end;
	
	if result = 0 then s^.login := login;
	
end;

procedure getStudentMark;
var
	
	i, savedIterator:integer value 0;
	mark:real value 0;
	n, result:integer;
	
begin
	
	case option of
		
		'f', 'j': n := 0;
		's': n := 1;
		'd': n := 2;
		
	end;
	
	if t then
	begin
		
		{*
			config es global, proporcionada por filesystem.
		*}
		
		if config[0] then
		begin
			
			for i := 0 to 2 do
			begin
				
				if mark < s^.marks[i].theory then
				begin
					
					savedIterator := i;
					mark := s^.marks[i].theory;
					
				end;
				
			end;
			
			if mark >= 5 then
			begin
				
				write('El alumno ya tiene aprobada la parte teórica: ', mark:0:1);
				
				case savedIterator of
					
					0: write('f/j');
					1: write('s');
					2: write('d');
					
				end;
				
				downLines(1);
				
			end;
			
		end;
		
		write('Nota teórica: ');
		
	end
	else
	begin
		
		if config[1] then
		begin
			
			mark := 0;
			
			for i := 0 to 2 do
			begin
				
				if mark < s^.marks[i].practice then
				begin
					
					savedIterator := i;
					mark := s^.marks[i].practice;
					
				end;
				
			end;
			
			if mark >= 5 then
			begin
				
				write('El alumno ya tiene aprobada la parte práctica: ', mark:0:1);
				
				case savedIterator of
					
					0: write('f/j');
					1: write('s');
					2: write('d');
					
				end;
				
				downLines(1);
				
			end;
			
		end;
		
		write('Nota práctica: ');
		
	end;
	
	result := readReal(mark, MARK_MAX, MARK_MIN, MARK_MAX_D);
	
	while result > 0 do
	begin
		
		case result of
			
			1: setError('Introduce un número entre 0 y 10, con un decimal como máximo.');
			2: setError('El número introducido no puede ser mayor que 10.');
			3: setError('El número introducido no puede ser menor que cero.');
			4: setError('El número introducido no puede tener más de una cifra decimal.');
			
		end;
		
		clearLine;
		
		if t then write('Nota teórica: ')
		else write('Nota práctica: ');
		
		result := readReal(mark, MARK_MAX, MARK_MIN, MARK_MAX_D);
		
	end;
	
	if mark = 0 then
	begin
		
		if t then
		begin
			
			if s^.marks[n].theory = 0 then setMssg('Nota teórica definida a 0 (valor por defecto).')
			else setMssg('La nota teórica no ha sido modificada.');
			
		end
		else
		begin
			
			if s^.marks[n].practice = 0 then setMssg('Nota práctica definida a 0 (valor por defecto).')
			else setMssg('La nota práctica no ha sido modificada.');
			
		end;
		
	end
	else
	begin
		
		if t then s^.marks[n].theory := mark
		else s^.marks[n].practice := mark;
		
	end;
	
end;

procedure getStudentMarks;
begin
	
	downLines(1);
	getStudentMark(s, option, false);
	downLines(1);
	getStudentMark(s, option, true);
	
end;

procedure getStudentMarksQC;
var
	
	option:char;
		
begin
	
	downLines(1);
	write('Convocatoria [f/j/s/d]: ');
	readChar(option);
	
	while not (option in MONTHS) do
	begin
		
		setError('Solo se admiten como valores válidos la inicial de los meses Febrero, Junio, Septiembre y Diciembre.');
		clearLine;
		write('Convocatoria [f/j/s/d]: ');
		readChar(option);
		
	end;
	
	getStudentMarks(s, option);
	
end;

function getStudentMarksQYN;
var
	
	i, savedIterator:integer;
	mark:real value 0;
	option:char;
		
begin
	
	if config[0] then
	begin
		
		for i := 0 to 2 do
		begin
			
			if mark < s^.marks[i].theory then
			begin
				
				savedIterator := i;
				mark := s^.marks[i].theory;
				
			end;
			
		end;
		
		if mark >= 5 then
		begin
			
			case savedIterator of
				
				0: write('El alumno aprobó la parte teórica en Febrero/Junio con un ', mark:0:1);
				1: write('El alumno aprobó la parte teórica en Septiembre con un ', mark:0:1);
				2: write('El alumno aprobó la parte teórica en Diciembre con un ', mark:0:1);
				
			end;
			
			downLines(1);
			
		end;
		
	end;
	
	if config[1] then
	begin
		
		mark := 0;
		
		for i := 0 to 2 do
		begin
			
			if mark < s^.marks[i].practice then
			begin
				
				savedIterator := i;
				mark := s^.marks[i].practice;
				
			end;
			
		end;
		
		if mark >= 5 then
		begin
			
			case savedIterator of
				
				0: write('El alumno aprobó la parte práctica en Febrero/Junio con un ', mark:0:1);
				1: write('El alumno aprobó la parte práctica en Septiembre con un ', mark:0:1);
				2: write('El alumno aprobó la parte práctica en Diciembre con un ', mark:0:1);
				
			end;
			
			downLines(1);
			
		end;
		
	end;
	
	if first then write('¿Introducir notas del alumno? [s/n]: ')
	else write('¿Seguir introduciendo notas? [s/n]: ');
	
	readChar(option);
	
	while not (option in YES_NO) do
	begin
		
		setError('Solo se admiten los valores "s" o "n", para responder "Sí" o "No" a la pregunta, respectivamente.');
		clearLine;
		write('¿Introducir notas del alumno? [s/n]: ');
		readChar(option);
		
	end;
	
	if option in YES then
	begin
		
		getStudentMarksQC(s);
		getStudentMarksQYN := true;
		
	end
	else getStudentMarksQYN := false;
	
end;

procedure getStudentName;
var
	
	temp:string(255);
	
begin
	
	write('Nombre: ');
	readln(temp);
	
	{*
		NAME_CAPACITY es una constante proporcionada por filesystem.
	*}
	if length(temp) > NAME_CAPACITY then
	begin
		
		setError('El nombre del alumno no puede superar los 20 caracteres.');
		clearLine;
		getStudentName(s);
		
	end
	else if temp <> '' then s^.name := temp;
	;
	
end;

procedure getStudentSurname;
var
	
	temp:string(255);
	
begin
	
	write('Apellidos: ');
	readln(temp);
	
	{*
		SURNAME_CAPACITY es una constante proporcionada por filesystem.
	*}
	if length(temp) > SURNAME_CAPACITY then
	begin
		
		setError('Los apellidos del alumno no pueden superar los 40 caracteres.');
		clearLine;
		getStudentSurname(s);
		
	end
	else if temp <> '' then s^.surname := temp;
	;
	
end;

function newStudent;
var
	
	b:boolean;
	s:PtrToStudent;
	
begin
	
	clearDisplay;
	setTitle('Nuevo alumno');
	setMssg('"Login" es el único dato obligatorio.');
	
	new(s);
	
	{*
		Inicializamos todos los campos de s^,
		pues podrían contener basura.
	*}
	s^.name := '';
	s^.surname := '';
	s^.login := '';
	s^.marks[0].theory := 0;
	s^.marks[0].practice := 0;
	s^.marks[1].theory := 0;
	s^.marks[1].practice := 0;
	s^.marks[2].theory := 0;
	s^.marks[2].practice := 0;
	
	getStudentName(s);
	downLines(1);
	
	getStudentSurname(s);
	downLines(1);
	
	getStudentLogin(s, false, true);
	downLines(1);
	
	b := getStudentMarksQYN(s, true);
	while b do
	begin
		
		downLines(-3);
		clearDown;
		b := getStudentMarksQYN(s, false);
		
	end;
	
	if data = NIL then newStudent := 4
	else
	begin
		
		extend(data^);
		write(data^, s^);
		
		newStudent := 3;
		
	end;
	
	dispose(s);
	
end;

procedure readChar;
begin
	
	c := input^;
	reset(input);
	
end;

function readLogin;
var
	
	cLetters, cNums:integer value 0;
	i:integer;
	temp:string(255);
	
begin
	
	write('[        ]');
	moveHorizontally(-9);
	
	readln(temp);
	
	if temp = '' then readLogin := 1
	else if length(temp) <> 8 then readLogin := 2
	else
	begin
		
		for i := 1 to 6 do if temp[i] in LETTERS then cLetters := cLetters + 1;
		for i := 7 to 8 do if temp[i] in NUMBERS then cNums := cNums + 1;
		
		if (cLetters = 6) and (cNums = 2) then
		begin
			
			if not search and studentExists(temp) then readLogin := 4
			else
			begin
				
				l := temp;
				readLogin := 0;
				
			end;
			
		end
		else readLogin := 3;
		
	end;
	;
	
end;

{*
	Esta función es una extensión de otra creada a partir de una
	sujerencia hecha en clase por el profesor, la cual en resumidas
	cuentas era que creásemos una función que permitiese
	leer de la entrada estándar hacia un real, pero evitando
	que se produjese un error de detención si lo introducido por
	el usuario no era realmente un real. La función original, con
	las explicaciones teóricas oportunas para comprender su
	funcionamiento, está incluída en ./doc/readReal.pas.
*}
function readReal;
var
	
	c, dCount:integer value 0;
	userInput:text;
	invalid, decimal:boolean value false;
	
begin
	
	rewrite(userInput);
	
	while not eoln and not invalid do
	begin
		
		c := c + 1;
		if decimal then dCount := dCount + 1;
		
		read(input, userInput^);
		
		{*
			Números del 0 al 9.
		*}
		if ((ord(userInput^) >= ZERO) and (ord(userInput^) <= NINE)) then put(userInput)
		{*
			El punto, siempre y cuando no haya sido introducido anteriormente y
			tampoco estemos hablando de que es el primer caracter.
		*}
		else if (userInput^ = POINT) and not decimal and (c <> 1) then
		begin
			
			put(userInput);
			decimal := true;
			
		end
		{*
			El signo, positivo o negativo, y pudiendo ocupar únicamente
			la primera posición.
		*}
		else if ((userInput^ = PLUS) or (userInput^ = MINUS)) and (c = 1) then put(userInput)
		else invalid := true;
			
		
		
	end;
	
	reset(input);
	reset(userInput);
	
	if invalid then readReal := 1
	else if eof(userInput) then readReal := 0
	else
	begin
		
		read(userInput, r);
		
		if r > max then readReal := 2
		else if r < min then readReal := 3
		else if dCount > dMax then readReal := 4
		else readReal := 0;
		;;
		
	end;
	;
	
end;

END.